@altruist03/ui 0.1.0 → 0.2.1

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 (207) hide show
  1. package/assets/images/noData.webp +0 -0
  2. package/dist/index.js +15237 -650
  3. package/dist/index.js.map +1 -1
  4. package/dist/noData-XPRHKMFR.webp +0 -0
  5. package/package.json +21 -7
  6. package/src/ThemeProvider/ThemeContext.tsx +75 -0
  7. package/src/ThemeProvider/index.ts +7 -0
  8. package/src/assets/images/noData.webp +0 -0
  9. package/src/assets.d.ts +10 -0
  10. package/src/components/Accordion/Accordion.stories.tsx +64 -0
  11. package/src/components/Accordion/Accordion.tsx +219 -0
  12. package/src/components/Accordion/Accordion.types.ts +16 -0
  13. package/src/components/Avatar/Avatar.stories.tsx +304 -0
  14. package/src/components/Avatar/Avatar.tsx +211 -0
  15. package/src/components/Avatar/Avatar.types.ts +33 -0
  16. package/src/components/Badge/Badge.stories.tsx +143 -0
  17. package/src/components/Badge/Badge.tsx +138 -0
  18. package/src/components/Badge/Badge.types.ts +24 -0
  19. package/src/components/Breadcrumb/Breadcrumb.stories.tsx +114 -0
  20. package/src/components/Breadcrumb/Breadcrumb.tsx +120 -0
  21. package/src/components/Breadcrumb/Breadcrumb.types.ts +22 -0
  22. package/src/components/Button/Button.stories.tsx +457 -0
  23. package/src/components/Button/Button.tsx +202 -0
  24. package/src/components/Button/Button.types.ts +18 -0
  25. package/src/components/Checkbox/Checkbox.stories.tsx +254 -0
  26. package/src/components/Checkbox/Checkbox.tsx +352 -0
  27. package/src/components/Checkbox/Checkbox.types.ts +123 -0
  28. package/src/components/CloseButton/CloseButton.stories.tsx +100 -0
  29. package/src/components/CloseButton/CloseButton.tsx +117 -0
  30. package/src/components/CloseButton/CloseButton.types.ts +15 -0
  31. package/src/components/CollapsibleSearch/CollapsibleSearch.tsx +120 -0
  32. package/src/components/DataTable/DataTable.stories.tsx +233 -0
  33. package/src/components/DataTable/DataTable.tsx +968 -0
  34. package/src/components/DataTable/DataTable.types.ts +49 -0
  35. package/src/components/DatePicker/DatePicker.stories.tsx +341 -0
  36. package/src/components/DatePicker/DatePicker.tsx +669 -0
  37. package/src/components/DatePicker/DatePicker.types.ts +105 -0
  38. package/src/components/DatePicker/DateTimePicker.tsx +35 -0
  39. package/src/components/DateRangePicker/DateRangePicker.tsx +179 -0
  40. package/src/components/DateRangePicker/DateRangePicker.types.ts +34 -0
  41. package/src/components/Drawer/Drawer.stories.tsx +243 -0
  42. package/src/components/Drawer/Drawer.tsx +196 -0
  43. package/src/components/Drawer/Drawer.types.ts +60 -0
  44. package/src/components/FileUpload/FileUpload.stories.tsx +162 -0
  45. package/src/components/FileUpload/FileUpload.tsx +216 -0
  46. package/src/components/FileUpload/FileUpload.types.ts +80 -0
  47. package/src/components/FilterPopover/FilterPopover.stories.tsx +173 -0
  48. package/src/components/FilterPopover/FilterPopover.tsx +310 -0
  49. package/src/components/FilterPopover/FilterPopover.types.ts +26 -0
  50. package/src/components/ImageViewer/ImageViewerToolbar.jsx +81 -0
  51. package/src/components/ImageViewer/useImagePanRotate.js +102 -0
  52. package/src/components/IpWhitelistField/IpWhitelistField.stories.tsx +138 -0
  53. package/src/components/IpWhitelistField/IpWhitelistField.tsx +164 -0
  54. package/src/components/IpWhitelistField/IpWhitelistField.types.ts +42 -0
  55. package/src/components/Loader/Loader.stories.tsx +95 -0
  56. package/src/components/Loader/Loader.tsx +132 -0
  57. package/src/components/Loader/Loader.types.ts +6 -0
  58. package/src/components/Menu/Menu.tsx +143 -0
  59. package/src/components/Menu/Menu.types.ts +28 -0
  60. package/src/components/MenuButton/MenuButton.stories.tsx +140 -0
  61. package/src/components/MenuButton/MenuButton.tsx +45 -0
  62. package/src/components/MenuButton/MenuButton.types.ts +21 -0
  63. package/src/components/Modal/Modal.stories.tsx +266 -0
  64. package/src/components/Modal/Modal.tsx +399 -0
  65. package/src/components/Modal/index.ts +2 -0
  66. package/src/components/Modal/types.ts +126 -0
  67. package/src/components/Pagination/Pagination.stories.tsx +167 -0
  68. package/src/components/Pagination/Pagination.tsx +266 -0
  69. package/src/components/Pagination/Pagination.types.ts +20 -0
  70. package/src/components/PeriodSelector/PeriodSelector.stories.tsx +50 -0
  71. package/src/components/PeriodSelector/PeriodSelector.tsx +47 -0
  72. package/src/components/PeriodSelector/PeriodSelector.types.ts +10 -0
  73. package/src/components/PhoneField/PhoneField.stories.tsx +141 -0
  74. package/src/components/PhoneField/PhoneField.tsx +284 -0
  75. package/src/components/PhoneField/PhoneField.types.ts +89 -0
  76. package/src/components/PhoneField/countryCodes.ts +236 -0
  77. package/src/components/Progress/Progress.stories.tsx +83 -0
  78. package/src/components/Progress/Progress.tsx +91 -0
  79. package/src/components/Progress/Progress.types.ts +22 -0
  80. package/src/components/RadioButton/RadioButton.stories.tsx +332 -0
  81. package/src/components/RadioButton/RadioButton.tsx +427 -0
  82. package/src/components/RadioButton/RadioButton.types.ts +117 -0
  83. package/src/components/ReportCard/ReportCard.tsx +183 -0
  84. package/src/components/ReportCard/ReportCard.types.ts +17 -0
  85. package/src/components/SectionIcon/SectionIcon.tsx +87 -0
  86. package/src/components/SectionIcon/SectionIcon.types.ts +23 -0
  87. package/src/components/SegmentedInputField/SegmentedInputField.stories.tsx +165 -0
  88. package/src/components/SegmentedInputField/SegmentedInputField.tsx +174 -0
  89. package/src/components/SegmentedInputField/SegmentedInputField.types.ts +51 -0
  90. package/src/components/SelectScroll/SelectScroll.stories.tsx +336 -0
  91. package/src/components/SelectScroll/SelectScroll.tsx +1119 -0
  92. package/src/components/SelectScroll/SelectScroll.types.ts +60 -0
  93. package/src/components/Selector/Selector.stories.tsx +382 -0
  94. package/src/components/Selector/Selector.tsx +1310 -0
  95. package/src/components/Selector/Selector.types.ts +71 -0
  96. package/src/components/Skeleton/Skeleton.stories.tsx +133 -0
  97. package/src/components/Skeleton/Skeleton.tsx +20 -0
  98. package/src/components/Skeleton/Skeleton.types.ts +3 -0
  99. package/src/components/Spinner/Spinner.stories.tsx +84 -0
  100. package/src/components/Spinner/Spinner.tsx +79 -0
  101. package/src/components/Spinner/Spinner.types.ts +8 -0
  102. package/src/components/StatCard/StatCard.stories.tsx +125 -0
  103. package/src/components/StatCard/StatCard.tsx +167 -0
  104. package/src/components/StatCard/StatCard.types.ts +20 -0
  105. package/src/components/StatusBadge/StatusBadge.stories.tsx +179 -0
  106. package/src/components/StatusBadge/StatusBadge.tsx +74 -0
  107. package/src/components/StatusBadge/StatusBadge.types.ts +23 -0
  108. package/src/components/Stepper/Stepper.stories.tsx +85 -0
  109. package/src/components/Stepper/Stepper.tsx +138 -0
  110. package/src/components/Stepper/Stepper.types.ts +16 -0
  111. package/src/components/SubTabs/SubTabs.stories.tsx +128 -0
  112. package/src/components/SubTabs/SubTabs.tsx +279 -0
  113. package/src/components/SubTabs/SubTabs.types.ts +44 -0
  114. package/src/components/SummaryCard/SummaryCard.stories.tsx +100 -0
  115. package/src/components/SummaryCard/SummaryCard.tsx +141 -0
  116. package/src/components/SummaryCard/SummaryCard.types.ts +22 -0
  117. package/src/components/Switch/Switch.stories.tsx +59 -0
  118. package/src/components/Switch/Switch.tsx +217 -0
  119. package/src/components/Switch/Switch.types.ts +22 -0
  120. package/src/components/Tabs/Tabs.stories.tsx +178 -0
  121. package/src/components/Tabs/Tabs.tsx +197 -0
  122. package/src/components/Tabs/Tabs.types.ts +34 -0
  123. package/src/components/TextField/TextField.stories.tsx +249 -0
  124. package/src/components/TextField/TextField.tsx +462 -0
  125. package/src/components/TextField/TextField.types.ts +141 -0
  126. package/src/components/Toast/Toast.stories.tsx +237 -0
  127. package/src/components/Toast/Toast.tsx +246 -0
  128. package/src/components/Toast/Toast.types.ts +31 -0
  129. package/src/components/ToggleButton/ToggleButton.stories.tsx +105 -0
  130. package/src/components/ToggleButton/ToggleButton.tsx +267 -0
  131. package/src/components/ToggleButton/ToggleButton.types.ts +19 -0
  132. package/src/components/UploadDropzone/UploadDropzone.jsx +78 -0
  133. package/src/components/UploadDropzone2/UploadDropzone.stories.tsx +94 -0
  134. package/src/components/UploadDropzone2/UploadDropzone.tsx +222 -0
  135. package/src/components/UploadDropzone2/UploadDropzone.types.ts +54 -0
  136. package/src/components/ViewEditToggle/ViewEditToggle.stories.tsx +40 -0
  137. package/src/components/ViewEditToggle/ViewEditToggle.tsx +239 -0
  138. package/src/components/ViewEditToggle/ViewEditToggle.types.ts +27 -0
  139. package/src/components/index.ts +157 -0
  140. package/src/icons/ReportDollarIcon.tsx +71 -0
  141. package/src/icons/index.ts +27 -0
  142. package/src/index.ts +214 -0
  143. package/src/storybook/preview.tsx +64 -0
  144. package/src/theme/index.ts +4 -0
  145. package/src/theme/types.d.ts +1 -0
  146. package/src/themes/cpc.ts +4 -0
  147. package/src/themes/firebird.ts +4 -0
  148. package/src/tokens/brands/cpc/themes/dark.json +82 -0
  149. package/src/tokens/brands/cpc/themes/light.json +1 -0
  150. package/src/tokens/brands/default/brand.json +20 -0
  151. package/src/tokens/brands/firebird/themes/dark.json +28 -0
  152. package/src/tokens/brands/firebird/themes/light.json +20 -0
  153. package/src/tokens/button.ts +24 -0
  154. package/src/tokens/components/accordion.json +52 -0
  155. package/src/tokens/components/avatar.json +40 -0
  156. package/src/tokens/components/badge.json +41 -0
  157. package/src/tokens/components/breadcrumb.json +13 -0
  158. package/src/tokens/components/button.json +67 -0
  159. package/src/tokens/components/card.json +39 -0
  160. package/src/tokens/components/checkbox.json +64 -0
  161. package/src/tokens/components/closebutton.json +55 -0
  162. package/src/tokens/components/datatable.json +40 -0
  163. package/src/tokens/components/datepicker.json +99 -0
  164. package/src/tokens/components/drawer.json +23 -0
  165. package/src/tokens/components/field.json +29 -0
  166. package/src/tokens/components/fileupload.json +57 -0
  167. package/src/tokens/components/filterpopover.json +29 -0
  168. package/src/tokens/components/input.json +31 -0
  169. package/src/tokens/components/ipwhitelistfield.json +16 -0
  170. package/src/tokens/components/loader.json +15 -0
  171. package/src/tokens/components/menu.json +21 -0
  172. package/src/tokens/components/modal.json +25 -0
  173. package/src/tokens/components/pagination.json +45 -0
  174. package/src/tokens/components/phonefield.json +41 -0
  175. package/src/tokens/components/progress.json +12 -0
  176. package/src/tokens/components/radio.json +87 -0
  177. package/src/tokens/components/reportcard.json +17 -0
  178. package/src/tokens/components/selector.json +87 -0
  179. package/src/tokens/components/selectscroll.json +65 -0
  180. package/src/tokens/components/skeleton.json +6 -0
  181. package/src/tokens/components/stepper.json +38 -0
  182. package/src/tokens/components/subtabs.json +45 -0
  183. package/src/tokens/components/switch.json +55 -0
  184. package/src/tokens/components/tabs.json +38 -0
  185. package/src/tokens/components/textfield.json +59 -0
  186. package/src/tokens/components/togglebutton.json +48 -0
  187. package/src/tokens/components/uploaddropzone.json +61 -0
  188. package/src/tokens/drawer.ts +37 -0
  189. package/src/tokens/foundation/breakpoints.json +10 -0
  190. package/src/tokens/foundation/colors.json +122 -0
  191. package/src/tokens/foundation/opacity.json +16 -0
  192. package/src/tokens/foundation/radius.json +14 -0
  193. package/src/tokens/foundation/size.json +38 -0
  194. package/src/tokens/foundation/spacing.json +19 -0
  195. package/src/tokens/foundation/typography.json +43 -0
  196. package/src/tokens/index.ts +3665 -0
  197. package/src/tokens/modal.ts +42 -0
  198. package/src/tokens/semantic/color.json +148 -0
  199. package/src/tokens/semantic/field.json +28 -0
  200. package/src/tokens/semantic/opacity.json +5 -0
  201. package/src/tokens/semantic/size.json +722 -0
  202. package/src/tokens/semantic/spacing.json +27 -0
  203. package/src/tokens/semantic/typography.json +87 -0
  204. package/src/utils/scrollLock.ts +44 -0
  205. package/dist/index.css +0 -514
  206. package/dist/index.css.map +0 -1
  207. package/dist/index.d.ts +0 -289
@@ -0,0 +1,173 @@
1
+ import React, { useState, useRef } from "react";
2
+ import type { Meta, StoryObj } from "@storybook/react";
3
+ import { FilterPopover } from "./FilterPopover";
4
+ import type { FilterPopoverProps } from "./FilterPopover.types";
5
+ import { MenuButton } from "../MenuButton/MenuButton";
6
+ import Selector from "../Selector/Selector";
7
+
8
+ const meta: Meta<typeof FilterPopover> = {
9
+ title: "Design System/FilterPopover",
10
+ component: FilterPopover,
11
+ tags: [],
12
+ decorators: [
13
+ (Story) => (
14
+ <div style={{ padding: "24px", maxWidth: "520px" }}>
15
+ <Story />
16
+ </div>
17
+ ),
18
+ ],
19
+ args: {
20
+ title: "Filter",
21
+ applyLabel: "Apply",
22
+ resetLabel: "Reset",
23
+ } as Partial<FilterPopoverProps>,
24
+ argTypes: {
25
+ title: {
26
+ control: { type: "text" },
27
+ },
28
+ applyLabel: {
29
+ control: { type: "text" },
30
+ },
31
+ resetLabel: {
32
+ control: { type: "text" },
33
+ },
34
+ },
35
+ };
36
+
37
+ export default meta;
38
+
39
+ type Story = StoryObj<typeof FilterPopover>;
40
+
41
+ function PlaygroundWrapper(props: Partial<FilterPopoverProps>) {
42
+ const anchorRef = useRef<HTMLDivElement>(null);
43
+ const [status, setStatus] = useState<string | null>("Verified");
44
+ const [category, setCategory] = useState<string | null>("Type");
45
+ const [type, setType] = useState<string | null>("All");
46
+
47
+ const handleReset = () => {
48
+ setStatus(null);
49
+ setCategory(null);
50
+ setType(null);
51
+ };
52
+
53
+ return (
54
+ <div ref={anchorRef} style={{ position: "relative", display: "inline-block", minHeight: 480 }}>
55
+ <FilterPopover
56
+ open={true}
57
+ onClose={() => {}}
58
+ anchorRef={anchorRef}
59
+ onReset={handleReset}
60
+ onApply={() => {}}
61
+ {...props}
62
+ >
63
+ <Selector
64
+ label="Status Type"
65
+ required
66
+ options={["Verified", "Pending", "Rejected"]}
67
+ value={status}
68
+ onChange={(v) => setStatus(v as string | null)}
69
+ placeholder="Select status"
70
+ size="md"
71
+ />
72
+ <Selector
73
+ label="Select Search Category"
74
+ required
75
+ options={["Type", "Name", "ID"]}
76
+ value={category}
77
+ onChange={(v) => setCategory(v as string | null)}
78
+ placeholder="Select category"
79
+ size="md"
80
+ />
81
+ <Selector
82
+ label="Select Type"
83
+ required
84
+ options={["All", "SMS", "WhatsApp", "Email"]}
85
+ value={type}
86
+ onChange={(v) => setType(v as string | null)}
87
+ placeholder="Select type"
88
+ size="md"
89
+ />
90
+ </FilterPopover>
91
+ </div>
92
+ );
93
+ }
94
+
95
+ export const Playground: Story = {
96
+ render: (args) => <PlaygroundWrapper {...args} />,
97
+ args: {
98
+ title: "Filter",
99
+ },
100
+ };
101
+
102
+ function WithMenuButtonExample() {
103
+ const [open, setOpen] = useState(false);
104
+ const anchorRef = useRef<HTMLDivElement>(null);
105
+ const [status, setStatus] = useState<string | null>("Verified");
106
+ const [category, setCategory] = useState<string | null>("Type");
107
+ const [type, setType] = useState<string | null>("All");
108
+
109
+ const handleReset = () => {
110
+ setStatus(null);
111
+ setCategory(null);
112
+ setType(null);
113
+ };
114
+
115
+ return (
116
+ <div ref={anchorRef} style={{ position: "relative", display: "inline-block" }}>
117
+ <MenuButton open={open} onClick={() => setOpen((o) => !o)} minWidth={140}>
118
+ Filter
119
+ </MenuButton>
120
+ <FilterPopover
121
+ open={open}
122
+ onClose={() => setOpen(false)}
123
+ anchorRef={anchorRef}
124
+ onReset={handleReset}
125
+ onApply={() => {}}
126
+ >
127
+ <Selector
128
+ label="Status Type"
129
+ required
130
+ options={["Verified", "Pending", "Rejected"]}
131
+ value={status}
132
+ onChange={(v) => setStatus(v as string | null)}
133
+ placeholder="Select status"
134
+ size="md"
135
+ />
136
+ <Selector
137
+ label="Select Search Category"
138
+ required
139
+ options={["Type", "Name", "ID"]}
140
+ value={category}
141
+ onChange={(v) => setCategory(v as string | null)}
142
+ placeholder="Select category"
143
+ size="md"
144
+ />
145
+ <Selector
146
+ label="Select Type"
147
+ required
148
+ options={["All", "SMS", "WhatsApp", "Email"]}
149
+ value={type}
150
+ onChange={(v) => setType(v as string | null)}
151
+ placeholder="Select type"
152
+ size="md"
153
+ />
154
+ </FilterPopover>
155
+ </div>
156
+ );
157
+ }
158
+
159
+ export const Showcase: Story = {
160
+ render: () => (
161
+ <div style={{ display: "flex", flexDirection: "column", gap: "48px" }}>
162
+ {/* Always open */}
163
+ <div style={{ minHeight: 480 }}>
164
+ <PlaygroundWrapper title="Filter" />
165
+ </div>
166
+
167
+ {/* With MenuButton trigger (click to open) */}
168
+ <div style={{ display: "flex", justifyContent: "flex-end" }}>
169
+ <WithMenuButtonExample />
170
+ </div>
171
+ </div>
172
+ ),
173
+ };
@@ -0,0 +1,310 @@
1
+ import React, { useEffect, useRef, useCallback, useState } from 'react';
2
+ import { createPortal } from 'react-dom';
3
+ import { CloseButton } from '../CloseButton/CloseButton';
4
+ import { Button } from '../Button/Button';
5
+ import type { FilterPopoverProps } from './FilterPopover.types';
6
+ import { useTokens } from '../../ThemeProvider/ThemeContext';
7
+ import { getFilterPopoverTokensFromResolved } from '../../tokens';
8
+
9
+ const VIEWPORT_EDGE_MARGIN_PX = 16;
10
+ const POPOVER_GAP_PX = 8;
11
+ const FILTER_POPOVER_Z_INDEX = 1300;
12
+
13
+ type PopoverPosition = {
14
+ top: number;
15
+ left: number;
16
+ width: number;
17
+ maxHeight: number;
18
+ };
19
+
20
+ function parseCssSize(value: string | number | undefined, viewportSize: number): number {
21
+ if (typeof value === 'number' && Number.isFinite(value)) return value;
22
+ if (value == null) return 0;
23
+
24
+ const normalized = String(value).trim();
25
+ if (!normalized) return 0;
26
+
27
+ if (normalized.endsWith('px')) {
28
+ const parsed = parseFloat(normalized);
29
+ return Number.isFinite(parsed) ? parsed : 0;
30
+ }
31
+
32
+ if (normalized.endsWith('vh')) {
33
+ const parsed = parseFloat(normalized);
34
+ return Number.isFinite(parsed) ? (parsed / 100) * viewportSize : 0;
35
+ }
36
+
37
+ if (/^\d+$/.test(normalized)) {
38
+ return parseInt(normalized, 10);
39
+ }
40
+
41
+ const calcMatch = normalized.match(/calc\(\s*100vh\s*-\s*(\d+)px\s*\)/i);
42
+ if (calcMatch) {
43
+ const offset = parseInt(calcMatch[1], 10);
44
+ return Number.isFinite(offset) ? Math.max(0, viewportSize - offset) : viewportSize;
45
+ }
46
+
47
+ const parsed = parseFloat(normalized);
48
+ return Number.isFinite(parsed) ? parsed : 0;
49
+ }
50
+
51
+ function getPopoverMinWidth(
52
+ minWidth: FilterPopoverProps['minWidth'],
53
+ tokenMinWidth: string | number | undefined,
54
+ viewportWidth: number
55
+ ): number {
56
+ const resolved =
57
+ minWidth ??
58
+ (typeof tokenMinWidth === 'string' && /^\d+$/.test(tokenMinWidth) ?
59
+ `${tokenMinWidth}px`
60
+ : tokenMinWidth);
61
+
62
+ const parsed = parseCssSize(resolved, viewportWidth);
63
+ return parsed > 0 ? parsed : 320;
64
+ }
65
+
66
+ export function FilterPopover({
67
+ open,
68
+ onClose,
69
+ anchorRef,
70
+ children,
71
+ onReset,
72
+ onApply,
73
+ applyLabel = 'Apply',
74
+ resetLabel = 'Reset',
75
+ minWidth,
76
+ minHeight,
77
+ title = 'Filter',
78
+ }: FilterPopoverProps) {
79
+ const popoverRef = useRef<HTMLDivElement>(null);
80
+ const contentRef = useRef<HTMLDivElement>(null);
81
+ const [popoverPosition, setPopoverPosition] = useState<PopoverPosition | null>(null);
82
+ const resolvedTokens = useTokens();
83
+ const tokens = getFilterPopoverTokensFromResolved(resolvedTokens);
84
+
85
+ const resolvedMinHeight =
86
+ minHeight ??
87
+ (typeof tokens.minHeight === 'string' && /^\d+$/.test(tokens.minHeight) ?
88
+ `${tokens.minHeight}px`
89
+ : tokens.minHeight);
90
+
91
+ const updatePosition = useCallback(() => {
92
+ const anchor = anchorRef.current;
93
+ const popover = popoverRef.current;
94
+ if (!anchor) return;
95
+
96
+ const rect = anchor.getBoundingClientRect();
97
+ const viewportWidth = Math.max(0, window.innerWidth || document.documentElement.clientWidth);
98
+ const viewportHeight = Math.max(0, window.innerHeight || document.documentElement.clientHeight);
99
+ const margin = VIEWPORT_EDGE_MARGIN_PX;
100
+ const gap = POPOVER_GAP_PX;
101
+
102
+ const minWidthPx = getPopoverMinWidth(minWidth, tokens.minWidth, viewportWidth);
103
+ const maxAllowedWidth = Math.max(0, viewportWidth - margin * 2);
104
+ const width = Math.min(Math.max(minWidthPx, rect.width), maxAllowedWidth);
105
+
106
+ let left = rect.right - width;
107
+ left = Math.max(margin, Math.min(left, viewportWidth - margin - width));
108
+
109
+ const maxHeightTokenPx = parseCssSize(tokens.maxHeight, viewportHeight) || viewportHeight * 0.6;
110
+ const measuredHeight = popover?.offsetHeight ?? 0;
111
+ const spaceBelow = Math.max(0, viewportHeight - rect.bottom - gap - margin);
112
+ const spaceAbove = Math.max(0, rect.top - gap - margin);
113
+
114
+ let top = rect.bottom + gap;
115
+ let maxHeight = Math.min(maxHeightTokenPx, spaceBelow);
116
+
117
+ if (measuredHeight > 0 && measuredHeight > spaceBelow && spaceAbove > spaceBelow) {
118
+ maxHeight = Math.min(maxHeightTokenPx, spaceAbove);
119
+ top = Math.max(margin, rect.top - gap - Math.min(measuredHeight, maxHeight));
120
+ }
121
+
122
+ setPopoverPosition({ top, left, width, maxHeight });
123
+ }, [anchorRef, minWidth, tokens.maxHeight, tokens.minWidth]);
124
+
125
+ const handleClickOutside = useCallback(
126
+ (e: MouseEvent) => {
127
+ const target = e.target as Node;
128
+ const element = target as Element;
129
+
130
+ const isInsideAnchor = anchorRef.current?.contains(target);
131
+ const isInsidePopover = popoverRef.current?.contains(target);
132
+
133
+ const isInsideSelectorListbox =
134
+ element?.closest?.('.selector-listbox') || element?.closest?.('.select-scroll-listbox');
135
+ const isInsideSelectorOption = element?.closest?.('[role="option"]');
136
+ const isInsideSelectorContainer = element?.closest?.('[role="combobox"]');
137
+
138
+ const isInsideDatePicker =
139
+ element?.closest?.('.react-datepicker-popper') ||
140
+ element?.closest?.('.react-datepicker') ||
141
+ element?.closest?.('#react-datepicker-portal');
142
+
143
+ if (
144
+ !isInsideAnchor &&
145
+ !isInsidePopover &&
146
+ !isInsideSelectorListbox &&
147
+ !isInsideSelectorOption &&
148
+ !isInsideSelectorContainer &&
149
+ !isInsideDatePicker
150
+ ) {
151
+ onClose();
152
+ }
153
+ },
154
+ [anchorRef, onClose]
155
+ );
156
+
157
+ useEffect(() => {
158
+ if (!open) {
159
+ setPopoverPosition(null);
160
+ return;
161
+ }
162
+
163
+ document.addEventListener('mousedown', handleClickOutside);
164
+ return () => document.removeEventListener('mousedown', handleClickOutside);
165
+ }, [open, handleClickOutside]);
166
+
167
+ useEffect(() => {
168
+ if (!open) return;
169
+
170
+ const runPositionUpdate = () => {
171
+ updatePosition();
172
+ requestAnimationFrame(updatePosition);
173
+ };
174
+
175
+ runPositionUpdate();
176
+
177
+ window.addEventListener('resize', runPositionUpdate);
178
+ window.addEventListener('scroll', runPositionUpdate, true);
179
+
180
+ const resizeObserver = new ResizeObserver(runPositionUpdate);
181
+ if (anchorRef.current) {
182
+ resizeObserver.observe(anchorRef.current);
183
+ }
184
+ if (popoverRef.current) {
185
+ resizeObserver.observe(popoverRef.current);
186
+ }
187
+ if (contentRef.current) {
188
+ resizeObserver.observe(contentRef.current);
189
+ }
190
+
191
+ return () => {
192
+ window.removeEventListener('resize', runPositionUpdate);
193
+ window.removeEventListener('scroll', runPositionUpdate, true);
194
+ resizeObserver.disconnect();
195
+ };
196
+ }, [open, updatePosition, anchorRef, children]);
197
+
198
+ if (!open || typeof document === 'undefined') return null;
199
+
200
+ const panelMinHeight =
201
+ typeof resolvedMinHeight === 'number' ? `${resolvedMinHeight}px` : resolvedMinHeight;
202
+
203
+ const panelStyle: React.CSSProperties = {
204
+ position: 'fixed',
205
+ top: popoverPosition?.top ?? -9999,
206
+ left: popoverPosition?.left ?? -9999,
207
+ width: popoverPosition?.width,
208
+ minWidth: popoverPosition?.width,
209
+ maxWidth: popoverPosition?.width,
210
+ minHeight: panelMinHeight,
211
+ maxHeight: popoverPosition?.maxHeight ?? tokens.maxHeight,
212
+ background: tokens.background,
213
+ border: tokens.border,
214
+ borderRadius: tokens.borderRadius,
215
+ boxShadow: tokens.boxShadow,
216
+ zIndex: FILTER_POPOVER_Z_INDEX,
217
+ display: 'flex',
218
+ flexDirection: 'column',
219
+ boxSizing: 'border-box',
220
+ overflow: 'hidden',
221
+ visibility: popoverPosition ? 'visible' : 'hidden',
222
+ };
223
+
224
+ return createPortal(
225
+ <div ref={popoverRef} role="dialog" aria-label={title} style={panelStyle}>
226
+ <div
227
+ style={{
228
+ display: 'flex',
229
+ alignItems: 'center',
230
+ justifyContent: 'space-between',
231
+ padding: tokens.headerPadding,
232
+ background: tokens.headerBackground,
233
+ marginBottom: tokens.headerMarginBottom,
234
+ borderRadius: tokens.innerBorderRadius,
235
+ flexShrink: 0,
236
+ }}
237
+ >
238
+ <span
239
+ style={{
240
+ margin: 0,
241
+ fontSize: tokens.headerTitleFontSize,
242
+ fontWeight: parseInt(tokens.headerTitleFontWeight, 10),
243
+ color: tokens.headerTitleColor,
244
+ lineHeight: '24px',
245
+ fontFamily: 'inherit',
246
+ }}
247
+ >
248
+ {title}
249
+ </span>
250
+ <CloseButton size="lg" variant="filled" aria-label="Close filter" onClick={onClose} />
251
+ </div>
252
+
253
+ <div
254
+ ref={contentRef}
255
+ style={{
256
+ flex: 1,
257
+ display: 'flex',
258
+ flexDirection: 'column',
259
+ gap: tokens.contentGap,
260
+ padding: tokens.contentPadding,
261
+ minHeight: 0,
262
+ overflowY: 'auto',
263
+ overflowX: 'hidden',
264
+ }}
265
+ >
266
+ {children}
267
+ </div>
268
+
269
+ {(onReset || onApply) && (
270
+ <div
271
+ style={{
272
+ display: 'flex',
273
+ justifyContent: 'space-between',
274
+ alignItems: 'center',
275
+ flexWrap: 'wrap',
276
+ gap: tokens.footerGap,
277
+ padding: tokens.footerPadding,
278
+ marginTop: tokens.footerMarginTop,
279
+ flexShrink: 0,
280
+ }}
281
+ >
282
+ {onReset && (
283
+ <Button
284
+ variant="grey"
285
+ size="sm"
286
+ onClick={() => {
287
+ onReset();
288
+ }}
289
+ >
290
+ {resetLabel}
291
+ </Button>
292
+ )}
293
+ {onApply && (
294
+ <Button
295
+ variant="blue"
296
+ size="sm"
297
+ onClick={() => {
298
+ onApply();
299
+ onClose();
300
+ }}
301
+ >
302
+ {applyLabel}
303
+ </Button>
304
+ )}
305
+ </div>
306
+ )}
307
+ </div>,
308
+ document.body
309
+ );
310
+ }
@@ -0,0 +1,26 @@
1
+ import type { ReactNode } from "react";
2
+
3
+ export interface FilterPopoverProps {
4
+ /** Whether the popover is open */
5
+ open: boolean;
6
+ /** Callback to close the popover */
7
+ onClose: () => void;
8
+ /** Ref to the anchor element (e.g. MenuButton wrapper) for positioning */
9
+ anchorRef: React.RefObject<HTMLElement | null>;
10
+ /** Filter fields and content - pass Selectors or custom form fields */
11
+ children: ReactNode;
12
+ /** Callback when Reset button is clicked */
13
+ onReset?: () => void;
14
+ /** Callback when Apply button is clicked */
15
+ onApply?: () => void;
16
+ /** Label for the Apply button */
17
+ applyLabel?: string;
18
+ /** Label for the Reset button */
19
+ resetLabel?: string;
20
+ /** Override minimum width */
21
+ minWidth?: number | string;
22
+ /** Override minimum height */
23
+ minHeight?: number | string;
24
+ /** Header title */
25
+ title?: string;
26
+ }
@@ -0,0 +1,81 @@
1
+ import React from 'react';
2
+
3
+ import RestartAltOutlinedIcon from '@mui/icons-material/RestartAltOutlined';
4
+ import RotateRightOutlinedIcon from '@mui/icons-material/RotateRightOutlined';
5
+ import ZoomInOutlinedIcon from '@mui/icons-material/ZoomInOutlined';
6
+ import ZoomOutOutlinedIcon from '@mui/icons-material/ZoomOutOutlined';
7
+ import { IconButton } from '@mui/material';
8
+
9
+ // Shared toolbar for image preview modals: zoom in/out, rotate and reset,
10
+ // each independently toggleable so callers can enable only what they need.
11
+ export default function ImageViewerToolbar({
12
+ isZoom = true,
13
+ isRotate = true,
14
+ isReset = true,
15
+ zoom = 1,
16
+ minZoom = 0.5,
17
+ maxZoom = 3,
18
+ onZoomIn,
19
+ onZoomOut,
20
+ onRotate,
21
+ onReset,
22
+ style,
23
+ className = '',
24
+ }) {
25
+ if (!isZoom && !isRotate && !isReset) return null;
26
+
27
+ return (
28
+ <div
29
+ className={['image-viewer-toolbar', className].filter(Boolean).join(' ')}
30
+ style={{
31
+ display: 'flex',
32
+ alignItems: 'center',
33
+ justifyContent: 'flex-end',
34
+ gap: 2,
35
+ ...style,
36
+ }}
37
+ >
38
+ {isRotate ?
39
+ <IconButton size="small" aria-label="Rotate image" onClick={onRotate}>
40
+ <RotateRightOutlinedIcon sx={{ fontSize: 20 }} />
41
+ </IconButton>
42
+ : null}
43
+ {isZoom ?
44
+ <>
45
+ <IconButton
46
+ size="small"
47
+ aria-label="Zoom out"
48
+ disabled={zoom <= minZoom}
49
+ onClick={onZoomOut}
50
+ >
51
+ <ZoomOutOutlinedIcon sx={{ fontSize: 20 }} />
52
+ </IconButton>
53
+ <span
54
+ style={{
55
+ minWidth: 44,
56
+ fontSize: 13,
57
+ fontWeight: 600,
58
+ color: '#374151',
59
+ textAlign: 'center',
60
+ }}
61
+ >
62
+ {Math.round(zoom * 100)}%
63
+ </span>
64
+ <IconButton
65
+ size="small"
66
+ aria-label="Zoom in"
67
+ disabled={zoom >= maxZoom}
68
+ onClick={onZoomIn}
69
+ >
70
+ <ZoomInOutlinedIcon sx={{ fontSize: 20 }} />
71
+ </IconButton>
72
+ </>
73
+ : null}
74
+ {isReset ?
75
+ <IconButton size="small" aria-label="Reset image" onClick={onReset}>
76
+ <RestartAltOutlinedIcon sx={{ fontSize: 20 }} />
77
+ </IconButton>
78
+ : null}
79
+ </div>
80
+ );
81
+ }
@@ -0,0 +1,102 @@
1
+ import { useCallback, useEffect, useRef, useState } from 'react';
2
+
3
+ // Shared drag-to-pan + rotate behaviour for image preview modals.
4
+ // Zoom stays owned by the caller (it is often lifted to a parent component
5
+ // so it can be reset alongside document/image navigation). Panning works by
6
+ // dragging the scrollable preview container itself (`containerRef`), so it
7
+ // composes with the existing width%-based zoom + native scrollbars instead
8
+ // of fighting them with a separate CSS transform.
9
+ /**
10
+ * @param {{
11
+ * isRotate?: boolean,
12
+ * showHand?: boolean,
13
+ * resetKey?: unknown,
14
+ * containerRef?: import('react').RefObject<HTMLElement>,
15
+ * }} [options]
16
+ */
17
+ export default function useImagePanRotate({
18
+ isRotate = true,
19
+ showHand = true,
20
+ resetKey,
21
+ containerRef,
22
+ } = {}) {
23
+ const [rotation, setRotation] = useState(0);
24
+ const [isDragging, setIsDragging] = useState(false);
25
+ const dragStateRef = useRef({ startX: 0, startY: 0, scrollLeft: 0, scrollTop: 0 });
26
+
27
+ // A new document/image should always open flat and scrolled to the top-left.
28
+ useEffect(() => {
29
+ setRotation(0);
30
+ setIsDragging(false);
31
+ const el = containerRef?.current;
32
+ if (el) {
33
+ el.scrollLeft = 0;
34
+ el.scrollTop = 0;
35
+ }
36
+ }, [resetKey]);
37
+
38
+ const rotate = useCallback(() => {
39
+ if (!isRotate) return;
40
+ setRotation((prev) => (prev + 90) % 360);
41
+ }, [isRotate]);
42
+
43
+ const resetTransform = useCallback(() => {
44
+ setRotation(0);
45
+ const el = containerRef?.current;
46
+ if (el) {
47
+ el.scrollLeft = 0;
48
+ el.scrollTop = 0;
49
+ }
50
+ }, [containerRef]);
51
+
52
+ const onMouseDown = useCallback(
53
+ (e) => {
54
+ if (!showHand) return;
55
+ const el = containerRef?.current;
56
+ if (!el) return;
57
+ e.preventDefault();
58
+ setIsDragging(true);
59
+ dragStateRef.current = {
60
+ startX: e.clientX,
61
+ startY: e.clientY,
62
+ scrollLeft: el.scrollLeft,
63
+ scrollTop: el.scrollTop,
64
+ };
65
+ },
66
+ [showHand, containerRef]
67
+ );
68
+
69
+ useEffect(() => {
70
+ if (!showHand || !isDragging) return undefined;
71
+
72
+ const handleMove = (e) => {
73
+ const el = containerRef?.current;
74
+ if (!el) return;
75
+ const dx = e.clientX - dragStateRef.current.startX;
76
+ const dy = e.clientY - dragStateRef.current.startY;
77
+ el.scrollLeft = dragStateRef.current.scrollLeft - dx;
78
+ el.scrollTop = dragStateRef.current.scrollTop - dy;
79
+ };
80
+ const handleUp = () => setIsDragging(false);
81
+
82
+ window.addEventListener('mousemove', handleMove);
83
+ window.addEventListener('mouseup', handleUp);
84
+ return () => {
85
+ window.removeEventListener('mousemove', handleMove);
86
+ window.removeEventListener('mouseup', handleUp);
87
+ };
88
+ }, [showHand, isDragging, containerRef]);
89
+
90
+ return {
91
+ rotation,
92
+ isDragging,
93
+ rotate,
94
+ resetTransform,
95
+ dragHandlers: showHand ? { onMouseDown } : {},
96
+ cursor:
97
+ showHand ?
98
+ isDragging ? 'grabbing'
99
+ : 'grab'
100
+ : 'default',
101
+ };
102
+ }