@snapdragonsnursery/react-components 1.21.1 → 1.23.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/package.json
CHANGED
package/src/ChildSearchModal.jsx
CHANGED
|
@@ -216,10 +216,10 @@ const ChildSearchModal = ({
|
|
|
216
216
|
}
|
|
217
217
|
|
|
218
218
|
// Handle status filtering
|
|
219
|
+
// Important: when status is "all", do not send active_only=true.
|
|
220
|
+
// The status dropdown is the source of truth for active/inactive/all.
|
|
219
221
|
if (advancedFilters.status && advancedFilters.status !== "all") {
|
|
220
222
|
params.append("status", advancedFilters.status);
|
|
221
|
-
} else if (activeOnly) {
|
|
222
|
-
params.append("active_only", "true");
|
|
223
223
|
}
|
|
224
224
|
|
|
225
225
|
// Add room filter
|
|
@@ -293,7 +293,6 @@ const ChildSearchModal = ({
|
|
|
293
293
|
pagination.pageSize,
|
|
294
294
|
siteId,
|
|
295
295
|
siteIds,
|
|
296
|
-
activeOnly,
|
|
297
296
|
advancedFilters,
|
|
298
297
|
applicationContext,
|
|
299
298
|
bypassPermissions,
|
package/src/ChildSearchPage.jsx
CHANGED
|
@@ -348,10 +348,13 @@ const ChildSearchPage = ({
|
|
|
348
348
|
}
|
|
349
349
|
|
|
350
350
|
// Handle status filtering
|
|
351
|
-
|
|
351
|
+
// Important: when status is "all", do not send active_only=true.
|
|
352
|
+
// The status dropdown is the source of truth for active/inactive/all.
|
|
353
|
+
if (
|
|
354
|
+
debouncedAdvancedFilters.status &&
|
|
355
|
+
debouncedAdvancedFilters.status !== "all"
|
|
356
|
+
) {
|
|
352
357
|
params.append("status", debouncedAdvancedFilters.status);
|
|
353
|
-
} else if (activeOnly) {
|
|
354
|
-
params.append("active_only", "true");
|
|
355
358
|
}
|
|
356
359
|
|
|
357
360
|
// Add room filter
|
|
@@ -431,7 +434,6 @@ const ChildSearchPage = ({
|
|
|
431
434
|
pagination.pageSize,
|
|
432
435
|
siteId,
|
|
433
436
|
siteIds,
|
|
434
|
-
activeOnly,
|
|
435
437
|
debouncedAdvancedFilters,
|
|
436
438
|
applicationContext,
|
|
437
439
|
bypassPermissions,
|
|
@@ -639,13 +639,13 @@ const EmployeeSearchModal = ({
|
|
|
639
639
|
}
|
|
640
640
|
|
|
641
641
|
// Handle status filtering
|
|
642
|
+
// Important: when status is "all", do not send active_only=true.
|
|
643
|
+
// The status dropdown is the source of truth for active/inactive/all.
|
|
642
644
|
if (
|
|
643
645
|
debouncedAdvancedFilters.status &&
|
|
644
646
|
debouncedAdvancedFilters.status !== "all"
|
|
645
647
|
) {
|
|
646
648
|
params.append("status", debouncedAdvancedFilters.status);
|
|
647
|
-
} else if (activeOnly) {
|
|
648
|
-
params.append("active_only", "true");
|
|
649
649
|
}
|
|
650
650
|
|
|
651
651
|
// Add role filter
|
|
@@ -759,7 +759,6 @@ const EmployeeSearchModal = ({
|
|
|
759
759
|
pagination.pageSize,
|
|
760
760
|
siteId,
|
|
761
761
|
siteIds,
|
|
762
|
-
activeOnly,
|
|
763
762
|
debouncedAdvancedFilters,
|
|
764
763
|
applicationContext,
|
|
765
764
|
bypassPermissions,
|
|
@@ -450,10 +450,13 @@ const EmployeeSearchPage = ({
|
|
|
450
450
|
}
|
|
451
451
|
|
|
452
452
|
// Handle status filtering
|
|
453
|
-
|
|
453
|
+
// Important: when status is "all", do not send active_only=true.
|
|
454
|
+
// The status dropdown is the source of truth for active/inactive/all.
|
|
455
|
+
if (
|
|
456
|
+
debouncedAdvancedFilters.status &&
|
|
457
|
+
debouncedAdvancedFilters.status !== "all"
|
|
458
|
+
) {
|
|
454
459
|
params.append("status", debouncedAdvancedFilters.status);
|
|
455
|
-
} else if (activeOnly) {
|
|
456
|
-
params.append("active_only", "true");
|
|
457
460
|
}
|
|
458
461
|
|
|
459
462
|
// Add role filter
|
|
@@ -581,7 +584,6 @@ const EmployeeSearchPage = ({
|
|
|
581
584
|
pagination.pageSize,
|
|
582
585
|
siteId,
|
|
583
586
|
siteIds,
|
|
584
|
-
activeOnly,
|
|
585
587
|
debouncedAdvancedFilters,
|
|
586
588
|
applicationContext,
|
|
587
589
|
bypassPermissions,
|
|
@@ -1,33 +1,35 @@
|
|
|
1
1
|
// FloatingActionButton.jsx
|
|
2
2
|
// ---------------------------------
|
|
3
3
|
// A shadcn-styled Floating Action Button (FAB).
|
|
4
|
-
// - Single
|
|
5
|
-
// -
|
|
6
|
-
//
|
|
7
|
-
// - Supports fixed positioning to screen corners
|
|
4
|
+
// - Single action mode: icon + onClick. Label reveal controlled by showLabel: 'hover' | 'always' | 'never'.
|
|
5
|
+
// - Menu mode: icon + actions array. FAB expands into a cohesive speed-dial; action items bloom
|
|
6
|
+
// out of the button with staggered animation. Icon swaps to X when open.
|
|
7
|
+
// - Supports fixed positioning to screen corners.
|
|
8
8
|
//
|
|
9
|
-
// Example usage:
|
|
9
|
+
// Example usage (single action):
|
|
10
10
|
//
|
|
11
|
-
//
|
|
12
|
-
// import { Plus } from "lucide-react";
|
|
11
|
+
// <FloatingActionButton icon={Plus} label="Create" onClick={() => {}} showLabel="hover" position="bottom-right" />
|
|
13
12
|
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
13
|
+
// Example usage (menu mode):
|
|
14
|
+
//
|
|
15
|
+
// <FloatingActionButton
|
|
16
|
+
// icon={Plus}
|
|
17
|
+
// actions={[
|
|
18
|
+
// { icon: FileText, label: "Send request form", onClick: () => {} },
|
|
19
|
+
// { icon: PenLine, label: "Log medication", onClick: () => {} },
|
|
20
|
+
// ]}
|
|
21
|
+
// position="bottom-right"
|
|
22
|
+
// menuAriaLabel="Medication actions"
|
|
23
|
+
// />
|
|
25
24
|
//
|
|
26
25
|
// Notes:
|
|
27
|
-
// - Provide ariaLabel
|
|
26
|
+
// - Provide ariaLabel (single) or menuAriaLabel (menu) when label is omitted.
|
|
28
27
|
// - Uses Tailwind CSS + shadcn design tokens (bg-primary, text-primary-foreground, etc.).
|
|
28
|
+
// - Menu mode renders a speed-dial: action items bloom vertically from the FAB with staggered
|
|
29
|
+
// transitions, keeping the whole group visually unified.
|
|
29
30
|
|
|
30
31
|
import * as React from "react";
|
|
32
|
+
import { X } from "lucide-react";
|
|
31
33
|
import { cn } from "../../lib/utils";
|
|
32
34
|
import { Button } from "./button.jsx";
|
|
33
35
|
|
|
@@ -39,22 +41,59 @@ const POSITION_CLASSES = {
|
|
|
39
41
|
none: "",
|
|
40
42
|
};
|
|
41
43
|
|
|
44
|
+
// For top positions, items bloom downward; for bottom positions, upward.
|
|
45
|
+
const isTopPosition = (position) =>
|
|
46
|
+
position === "top-right" || position === "top-left";
|
|
47
|
+
|
|
48
|
+
// Label appears on the opposite side of the screen edge.
|
|
49
|
+
const labelSide = (position) =>
|
|
50
|
+
position === "bottom-right" || position === "top-right" ? "left" : "right";
|
|
51
|
+
|
|
42
52
|
function FloatingActionButton({
|
|
43
53
|
icon: Icon,
|
|
44
54
|
label = "",
|
|
45
55
|
onClick,
|
|
56
|
+
actions,
|
|
46
57
|
showLabel,
|
|
47
|
-
// Deprecated: expandOnHover kept for backwards compatibility.
|
|
48
|
-
// we map: true -> 'hover', false -> 'never'.
|
|
58
|
+
// Deprecated: expandOnHover kept for backwards compatibility.
|
|
49
59
|
expandOnHover,
|
|
50
60
|
position = "bottom-right",
|
|
51
61
|
className,
|
|
52
62
|
disabled = false,
|
|
53
63
|
ariaLabel,
|
|
64
|
+
menuAriaLabel,
|
|
54
65
|
positionClassName,
|
|
55
66
|
colorClassName,
|
|
67
|
+
menuButtonClassName,
|
|
68
|
+
menuLabelClassName,
|
|
56
69
|
...props
|
|
57
70
|
}) {
|
|
71
|
+
const [menuOpen, setMenuOpen] = React.useState(false);
|
|
72
|
+
const isMenuMode = actions != null && actions.length > 0;
|
|
73
|
+
const containerRef = React.useRef(null);
|
|
74
|
+
|
|
75
|
+
// Close on outside click
|
|
76
|
+
React.useEffect(() => {
|
|
77
|
+
if (!menuOpen) return;
|
|
78
|
+
const handler = (e) => {
|
|
79
|
+
if (containerRef.current && !containerRef.current.contains(e.target)) {
|
|
80
|
+
setMenuOpen(false);
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
document.addEventListener("pointerdown", handler);
|
|
84
|
+
return () => document.removeEventListener("pointerdown", handler);
|
|
85
|
+
}, [menuOpen]);
|
|
86
|
+
|
|
87
|
+
// Close on Escape
|
|
88
|
+
React.useEffect(() => {
|
|
89
|
+
if (!menuOpen) return;
|
|
90
|
+
const handler = (e) => {
|
|
91
|
+
if (e.key === "Escape") setMenuOpen(false);
|
|
92
|
+
};
|
|
93
|
+
document.addEventListener("keydown", handler);
|
|
94
|
+
return () => document.removeEventListener("keydown", handler);
|
|
95
|
+
}, [menuOpen]);
|
|
96
|
+
|
|
58
97
|
const resolvedShowLabel = React.useMemo(() => {
|
|
59
98
|
if (showLabel) return showLabel;
|
|
60
99
|
if (typeof expandOnHover === "boolean") {
|
|
@@ -65,6 +104,7 @@ function FloatingActionButton({
|
|
|
65
104
|
|
|
66
105
|
const containerClasses = cn(
|
|
67
106
|
positionClassName || (POSITION_CLASSES[position ?? "none"] ?? POSITION_CLASSES.none),
|
|
107
|
+
"z-50",
|
|
68
108
|
className
|
|
69
109
|
);
|
|
70
110
|
|
|
@@ -72,20 +112,30 @@ function FloatingActionButton({
|
|
|
72
112
|
"rounded-full shadow-lg focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 leading-none",
|
|
73
113
|
colorClassName || "bg-primary hover:bg-primary/90 text-primary-foreground"
|
|
74
114
|
);
|
|
115
|
+
const menuButtonBaseClass = cn(
|
|
116
|
+
"bg-secondary hover:bg-secondary/80 text-secondary-foreground",
|
|
117
|
+
"hover:scale-110 hover:shadow-lg hover:ring-2 hover:ring-primary/40",
|
|
118
|
+
menuButtonClassName
|
|
119
|
+
);
|
|
120
|
+
const menuLabelBaseClass = cn(
|
|
121
|
+
"bg-popover text-popover-foreground border border-border",
|
|
122
|
+
menuLabelClassName
|
|
123
|
+
);
|
|
75
124
|
|
|
76
125
|
const shapeClasses =
|
|
77
|
-
resolvedShowLabel === "never"
|
|
126
|
+
resolvedShowLabel === "never" || isMenuMode
|
|
78
127
|
? "h-14 w-14 inline-flex items-center justify-center gap-0 p-0"
|
|
79
128
|
: resolvedShowLabel === "always"
|
|
80
129
|
? "h-14 inline-flex items-center justify-center gap-0 px-4"
|
|
81
|
-
:
|
|
82
|
-
"group h-14 inline-flex items-center gap-0 justify-center min-w-[3.5rem] px-0 transition-all duration-300 ease-out hover:px-4";
|
|
130
|
+
: "group h-14 inline-flex items-center gap-0 justify-center min-w-[3.5rem] px-0 transition-all duration-300 ease-out hover:px-4";
|
|
83
131
|
|
|
84
132
|
const buttonClasses = cn(baseButton, shapeClasses);
|
|
85
133
|
|
|
86
134
|
const iconClasses = cn(
|
|
87
|
-
"h-6 w-6 block shrink-0 pointer-events-none
|
|
88
|
-
resolvedShowLabel === "hover"
|
|
135
|
+
"h-6 w-6 block shrink-0 pointer-events-none",
|
|
136
|
+
!isMenuMode && resolvedShowLabel === "hover"
|
|
137
|
+
? "group-hover:scale-110 group-hover:rotate-3 transition-transform duration-200"
|
|
138
|
+
: ""
|
|
89
139
|
);
|
|
90
140
|
|
|
91
141
|
const labelClasses = cn(
|
|
@@ -97,6 +147,151 @@ function FloatingActionButton({
|
|
|
97
147
|
: "sr-only"
|
|
98
148
|
);
|
|
99
149
|
|
|
150
|
+
// ── MENU (speed-dial) MODE ───────────────────────────────────────────────
|
|
151
|
+
if (isMenuMode) {
|
|
152
|
+
const top = isTopPosition(position);
|
|
153
|
+
const labelOnLeft = labelSide(position) === "left";
|
|
154
|
+
const columnAlignClass = labelOnLeft ? "items-end" : "items-start";
|
|
155
|
+
const actionListMarginClass = top ? "mt-3" : "mb-3";
|
|
156
|
+
const hiddenTranslateYClass = top ? "-translate-y-4" : "translate-y-4";
|
|
157
|
+
|
|
158
|
+
const getStaggerDelay = (idx) => {
|
|
159
|
+
if (menuOpen) {
|
|
160
|
+
// Bloom from the trigger button outwards.
|
|
161
|
+
// Top menus: idx=0 is nearest; bottom menus: idx=last is nearest.
|
|
162
|
+
return top ? idx * 40 : (actions.length - 1 - idx) * 40;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Reverse order while collapsing for a cleaner close animation.
|
|
166
|
+
return top ? (actions.length - 1 - idx) * 40 : idx * 40;
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
const renderActionList = () => (
|
|
170
|
+
<div
|
|
171
|
+
className={cn("flex gap-3 flex-col", actionListMarginClass, columnAlignClass)}
|
|
172
|
+
aria-hidden={!menuOpen}
|
|
173
|
+
>
|
|
174
|
+
{actions.map((action, idx) => {
|
|
175
|
+
const ActionIcon = action.icon;
|
|
176
|
+
const staggerDelay = getStaggerDelay(idx);
|
|
177
|
+
|
|
178
|
+
return (
|
|
179
|
+
<div
|
|
180
|
+
key={idx}
|
|
181
|
+
className={cn(
|
|
182
|
+
"flex items-center gap-3",
|
|
183
|
+
labelOnLeft ? "flex-row" : "flex-row-reverse"
|
|
184
|
+
)}
|
|
185
|
+
style={{ transitionDelay: `${staggerDelay}ms` }}
|
|
186
|
+
>
|
|
187
|
+
<span
|
|
188
|
+
className={cn(
|
|
189
|
+
"px-3 py-1.5 rounded-full text-sm font-medium whitespace-nowrap",
|
|
190
|
+
"shadow-md",
|
|
191
|
+
menuLabelBaseClass,
|
|
192
|
+
"transition-all duration-200 ease-out",
|
|
193
|
+
menuOpen
|
|
194
|
+
? "opacity-100 translate-x-0 scale-100"
|
|
195
|
+
: labelOnLeft
|
|
196
|
+
? "opacity-0 translate-x-3 scale-95 pointer-events-none"
|
|
197
|
+
: "opacity-0 -translate-x-3 scale-95 pointer-events-none"
|
|
198
|
+
)}
|
|
199
|
+
style={{ transitionDelay: `${staggerDelay}ms` }}
|
|
200
|
+
>
|
|
201
|
+
{action.label}
|
|
202
|
+
</span>
|
|
203
|
+
|
|
204
|
+
<button
|
|
205
|
+
type="button"
|
|
206
|
+
onClick={() => {
|
|
207
|
+
action.onClick?.();
|
|
208
|
+
setMenuOpen(false);
|
|
209
|
+
}}
|
|
210
|
+
disabled={disabled}
|
|
211
|
+
tabIndex={menuOpen ? 0 : -1}
|
|
212
|
+
aria-label={action.label}
|
|
213
|
+
title={action.label}
|
|
214
|
+
className={cn(
|
|
215
|
+
"group h-12 w-12 rounded-full shadow-md flex items-center justify-center shrink-0",
|
|
216
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
|
217
|
+
"transition-all duration-200 ease-out",
|
|
218
|
+
menuButtonBaseClass,
|
|
219
|
+
menuOpen
|
|
220
|
+
? "opacity-100 scale-100 translate-y-0"
|
|
221
|
+
: cn("opacity-0 scale-75 pointer-events-none", hiddenTranslateYClass)
|
|
222
|
+
)}
|
|
223
|
+
style={{ transitionDelay: `${staggerDelay}ms` }}
|
|
224
|
+
>
|
|
225
|
+
{ActionIcon ? (
|
|
226
|
+
<ActionIcon
|
|
227
|
+
className="h-5 w-5 pointer-events-none transition-transform duration-200 group-hover:scale-110 group-hover:rotate-6"
|
|
228
|
+
aria-hidden="true"
|
|
229
|
+
/>
|
|
230
|
+
) : null}
|
|
231
|
+
</button>
|
|
232
|
+
</div>
|
|
233
|
+
);
|
|
234
|
+
})}
|
|
235
|
+
</div>
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
const renderTriggerButton = () => (
|
|
239
|
+
<Button
|
|
240
|
+
type="button"
|
|
241
|
+
data-slot="floating-action-button"
|
|
242
|
+
className={cn(buttonClasses, "transition-transform duration-200", menuOpen ? "rotate-0" : "")}
|
|
243
|
+
size="icon"
|
|
244
|
+
aria-label={menuAriaLabel ?? ariaLabel ?? "Open menu"}
|
|
245
|
+
aria-expanded={menuOpen}
|
|
246
|
+
aria-haspopup="true"
|
|
247
|
+
title={menuAriaLabel ?? ariaLabel ?? "Open menu"}
|
|
248
|
+
disabled={disabled}
|
|
249
|
+
onClick={() => setMenuOpen((v) => !v)}
|
|
250
|
+
{...props}
|
|
251
|
+
>
|
|
252
|
+
<span
|
|
253
|
+
className={cn(
|
|
254
|
+
"transition-all duration-300 absolute",
|
|
255
|
+
menuOpen ? "opacity-100 rotate-0 scale-100" : "opacity-0 rotate-90 scale-75"
|
|
256
|
+
)}
|
|
257
|
+
aria-hidden="true"
|
|
258
|
+
>
|
|
259
|
+
<X className="h-6 w-6" />
|
|
260
|
+
</span>
|
|
261
|
+
<span
|
|
262
|
+
className={cn(
|
|
263
|
+
"transition-all duration-300",
|
|
264
|
+
menuOpen ? "opacity-0 -rotate-90 scale-75" : "opacity-100 rotate-0 scale-100"
|
|
265
|
+
)}
|
|
266
|
+
aria-hidden="true"
|
|
267
|
+
>
|
|
268
|
+
{Icon ? <Icon className="h-6 w-6 block shrink-0" /> : null}
|
|
269
|
+
</span>
|
|
270
|
+
</Button>
|
|
271
|
+
);
|
|
272
|
+
|
|
273
|
+
return (
|
|
274
|
+
<div
|
|
275
|
+
ref={containerRef}
|
|
276
|
+
className={cn(containerClasses, "flex flex-col gap-0", columnAlignClass)}
|
|
277
|
+
data-testid="fab-container"
|
|
278
|
+
>
|
|
279
|
+
{top ? (
|
|
280
|
+
<>
|
|
281
|
+
{renderTriggerButton()}
|
|
282
|
+
{renderActionList()}
|
|
283
|
+
</>
|
|
284
|
+
) : (
|
|
285
|
+
<>
|
|
286
|
+
{renderActionList()}
|
|
287
|
+
{renderTriggerButton()}
|
|
288
|
+
</>
|
|
289
|
+
)}
|
|
290
|
+
</div>
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// ── SINGLE ACTION MODE ───────────────────────────────────────────────────
|
|
100
295
|
return (
|
|
101
296
|
<div className={containerClasses} data-testid="fab-container">
|
|
102
297
|
<Button
|
package/src/index.d.ts
CHANGED
|
@@ -123,6 +123,11 @@ export interface FloatingActionButtonProps {
|
|
|
123
123
|
icon?: React.ComponentType<any>
|
|
124
124
|
label?: string
|
|
125
125
|
onClick?: () => void
|
|
126
|
+
actions?: Array<{
|
|
127
|
+
icon?: React.ComponentType<any>
|
|
128
|
+
label: string
|
|
129
|
+
onClick?: () => void
|
|
130
|
+
}>
|
|
126
131
|
showLabel?: 'hover' | 'always' | 'never'
|
|
127
132
|
// @deprecated use showLabel instead. If provided: true => 'hover', false => 'never'
|
|
128
133
|
expandOnHover?: boolean
|
|
@@ -130,8 +135,11 @@ export interface FloatingActionButtonProps {
|
|
|
130
135
|
className?: string
|
|
131
136
|
positionClassName?: string
|
|
132
137
|
colorClassName?: string
|
|
138
|
+
menuButtonClassName?: string
|
|
139
|
+
menuLabelClassName?: string
|
|
133
140
|
disabled?: boolean
|
|
134
141
|
ariaLabel?: string
|
|
142
|
+
menuAriaLabel?: string
|
|
135
143
|
}
|
|
136
144
|
export const FloatingActionButton: React.ComponentType<FloatingActionButtonProps>
|
|
137
145
|
|