@exitvibing/hqui 0.1.0 → 0.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/README.md +13 -7
- package/dist/index.css +1 -1
- package/dist/index.d.mts +18 -16
- package/dist/index.d.ts +18 -16
- package/dist/index.js +137 -94
- package/dist/index.mjs +141 -93
- package/docs/extended-components.md +50 -41
- package/package.json +1 -1
- package/src/components/extended/FloatingPopup.tsx +139 -0
- package/src/components/extended/Sidebar.tsx +64 -0
- package/src/index.css +91 -0
- package/src/index.ts +2 -1
- package/src/components/extended/WeekViewCalendar.tsx +0 -126
package/dist/index.mjs
CHANGED
|
@@ -849,108 +849,155 @@ var StatusBar = forwardRef18(
|
|
|
849
849
|
);
|
|
850
850
|
StatusBar.displayName = "StatusBar";
|
|
851
851
|
|
|
852
|
-
// src/components/extended/
|
|
852
|
+
// src/components/extended/Sidebar.tsx
|
|
853
853
|
import { forwardRef as forwardRef19 } from "react";
|
|
854
|
+
import { ChevronLeft as ChevronLeft2, ChevronRight as ChevronRight2 } from "lucide-react";
|
|
854
855
|
import { jsx as jsx19, jsxs as jsxs10 } from "react/jsx-runtime";
|
|
855
|
-
var
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
children: [
|
|
871
|
-
/* @__PURE__ */ jsxs10("div", { className: "grid grid-cols-8 border-b border-border bg-muted/30", children: [
|
|
872
|
-
/* @__PURE__ */ jsx19("div", { className: "flex items-center justify-center border-r border-border p-2 text-xs font-medium text-muted-foreground w-16", children: "Time" }),
|
|
873
|
-
days.map((day, i) => /* @__PURE__ */ jsx19(
|
|
874
|
-
"div",
|
|
875
|
-
{
|
|
876
|
-
className: cn(
|
|
877
|
-
"p-2 text-center text-sm font-semibold",
|
|
878
|
-
i < 6 && "border-r border-border"
|
|
879
|
-
),
|
|
880
|
-
children: day
|
|
881
|
-
},
|
|
882
|
-
day
|
|
883
|
-
))
|
|
884
|
-
] }),
|
|
885
|
-
/* @__PURE__ */ jsxs10("div", { className: "relative overflow-y-auto max-h-[500px] custom-scrollbar", children: [
|
|
886
|
-
hours.map((hour, hIndex) => /* @__PURE__ */ jsxs10(
|
|
856
|
+
var Sidebar = forwardRef19(
|
|
857
|
+
({ className, side, open, onToggle, width = 240, children, style, ...props }, ref) => {
|
|
858
|
+
const isLeft = side === "left";
|
|
859
|
+
return /* @__PURE__ */ jsxs10(
|
|
860
|
+
"div",
|
|
861
|
+
{
|
|
862
|
+
ref,
|
|
863
|
+
className: cn("sidebar-root", `sidebar-${side}`, className),
|
|
864
|
+
style: {
|
|
865
|
+
"--sidebar-w": `${width}px`,
|
|
866
|
+
...style
|
|
867
|
+
},
|
|
868
|
+
...props,
|
|
869
|
+
children: [
|
|
870
|
+
/* @__PURE__ */ jsx19(
|
|
887
871
|
"div",
|
|
888
872
|
{
|
|
889
873
|
className: cn(
|
|
890
|
-
"
|
|
891
|
-
|
|
874
|
+
"sidebar-panel",
|
|
875
|
+
open ? "sidebar-panel-open" : "sidebar-panel-closed"
|
|
892
876
|
),
|
|
893
|
-
children:
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
"div",
|
|
897
|
-
{
|
|
898
|
-
className: cn(
|
|
899
|
-
"h-16 relative",
|
|
900
|
-
dIndex < 6 && "border-r border-border/50"
|
|
901
|
-
)
|
|
902
|
-
},
|
|
903
|
-
dIndex
|
|
904
|
-
))
|
|
905
|
-
]
|
|
906
|
-
},
|
|
907
|
-
hour
|
|
908
|
-
)),
|
|
877
|
+
children: /* @__PURE__ */ jsx19("div", { className: "sidebar-content", children })
|
|
878
|
+
}
|
|
879
|
+
),
|
|
909
880
|
/* @__PURE__ */ jsx19(
|
|
910
|
-
"
|
|
881
|
+
"button",
|
|
911
882
|
{
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
const top = (event.startHour - startHour) / (endHour - startHour) * 100;
|
|
918
|
-
const height = event.durationHours / (endHour - startHour) * 100;
|
|
919
|
-
const left = event.dayIndex / 7 * 100;
|
|
920
|
-
const width = 100 / 7;
|
|
921
|
-
return /* @__PURE__ */ jsx19(
|
|
922
|
-
"div",
|
|
923
|
-
{
|
|
924
|
-
className: "absolute p-0.5 pointer-events-auto",
|
|
925
|
-
style: {
|
|
926
|
-
top: `${top}%`,
|
|
927
|
-
left: `${left}%`,
|
|
928
|
-
height: `${height}%`,
|
|
929
|
-
width: `${width}%`
|
|
930
|
-
},
|
|
931
|
-
children: /* @__PURE__ */ jsx19(
|
|
932
|
-
"div",
|
|
933
|
-
{
|
|
934
|
-
className: "h-full w-full rounded-md border border-white/10 p-1.5 text-xs font-semibold text-white overflow-hidden shadow-sm transition-transform hover:scale-[1.02] hover:shadow-md cursor-pointer",
|
|
935
|
-
style: {
|
|
936
|
-
backgroundColor: event.color || "hsl(var(--primary))"
|
|
937
|
-
},
|
|
938
|
-
title: event.title,
|
|
939
|
-
children: event.title
|
|
940
|
-
}
|
|
941
|
-
)
|
|
942
|
-
},
|
|
943
|
-
event.id
|
|
944
|
-
);
|
|
945
|
-
}) })
|
|
883
|
+
type: "button",
|
|
884
|
+
onClick: onToggle,
|
|
885
|
+
className: "sidebar-toggle",
|
|
886
|
+
"aria-label": open ? `Hide ${side} sidebar` : `Show ${side} sidebar`,
|
|
887
|
+
children: isLeft ? open ? /* @__PURE__ */ jsx19(ChevronLeft2, { className: "h-4 w-4" }) : /* @__PURE__ */ jsx19(ChevronRight2, { className: "h-4 w-4" }) : open ? /* @__PURE__ */ jsx19(ChevronRight2, { className: "h-4 w-4" }) : /* @__PURE__ */ jsx19(ChevronLeft2, { className: "h-4 w-4" })
|
|
946
888
|
}
|
|
947
889
|
)
|
|
948
|
-
]
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
890
|
+
]
|
|
891
|
+
}
|
|
892
|
+
);
|
|
893
|
+
}
|
|
894
|
+
);
|
|
895
|
+
Sidebar.displayName = "Sidebar";
|
|
896
|
+
|
|
897
|
+
// src/components/extended/FloatingPopup.tsx
|
|
898
|
+
import {
|
|
899
|
+
forwardRef as forwardRef20,
|
|
900
|
+
useEffect as useEffect4,
|
|
901
|
+
useLayoutEffect,
|
|
902
|
+
useState as useState5,
|
|
903
|
+
useCallback as useCallback2
|
|
904
|
+
} from "react";
|
|
905
|
+
import { X as X2 } from "lucide-react";
|
|
906
|
+
import { jsx as jsx20, jsxs as jsxs11 } from "react/jsx-runtime";
|
|
907
|
+
var FloatingPopup = forwardRef20(
|
|
908
|
+
({ className, open, onClose, anchorRef, title, gap = 8, children, ...props }, ref) => {
|
|
909
|
+
const [pos, setPos] = useState5(null);
|
|
910
|
+
const [visible, setVisible] = useState5(false);
|
|
911
|
+
const calculate = useCallback2(() => {
|
|
912
|
+
if (!anchorRef.current) return;
|
|
913
|
+
const rect = anchorRef.current.getBoundingClientRect();
|
|
914
|
+
const popupHeight = 200;
|
|
915
|
+
const popupWidth = 320;
|
|
916
|
+
const spaceBelow = window.innerHeight - rect.bottom;
|
|
917
|
+
const direction = spaceBelow >= popupHeight + gap ? "below" : "above";
|
|
918
|
+
let top;
|
|
919
|
+
if (direction === "below") {
|
|
920
|
+
top = rect.bottom + gap;
|
|
921
|
+
} else {
|
|
922
|
+
top = rect.top - popupHeight - gap;
|
|
923
|
+
if (top < 8) top = 8;
|
|
924
|
+
}
|
|
925
|
+
let left = rect.left + rect.width / 2 - popupWidth / 2;
|
|
926
|
+
if (left < 8) left = 8;
|
|
927
|
+
if (left + popupWidth > window.innerWidth - 8) {
|
|
928
|
+
left = window.innerWidth - popupWidth - 8;
|
|
929
|
+
}
|
|
930
|
+
setPos({ top, left, direction });
|
|
931
|
+
}, [anchorRef, gap]);
|
|
932
|
+
useLayoutEffect(() => {
|
|
933
|
+
if (open) {
|
|
934
|
+
calculate();
|
|
935
|
+
requestAnimationFrame(() => setVisible(true));
|
|
936
|
+
} else {
|
|
937
|
+
setVisible(false);
|
|
938
|
+
setPos(null);
|
|
939
|
+
}
|
|
940
|
+
}, [open, calculate]);
|
|
941
|
+
useEffect4(() => {
|
|
942
|
+
if (!open) return;
|
|
943
|
+
const onResize = () => calculate();
|
|
944
|
+
const onScroll = () => calculate();
|
|
945
|
+
window.addEventListener("resize", onResize);
|
|
946
|
+
window.addEventListener("scroll", onScroll, true);
|
|
947
|
+
return () => {
|
|
948
|
+
window.removeEventListener("resize", onResize);
|
|
949
|
+
window.removeEventListener("scroll", onScroll, true);
|
|
950
|
+
};
|
|
951
|
+
}, [open, calculate]);
|
|
952
|
+
useEffect4(() => {
|
|
953
|
+
if (!open) return;
|
|
954
|
+
const handleEsc = (e) => {
|
|
955
|
+
if (e.key === "Escape") onClose();
|
|
956
|
+
};
|
|
957
|
+
document.addEventListener("keydown", handleEsc);
|
|
958
|
+
return () => document.removeEventListener("keydown", handleEsc);
|
|
959
|
+
}, [open, onClose]);
|
|
960
|
+
if (!open || !pos) return null;
|
|
961
|
+
return /* @__PURE__ */ jsxs11(
|
|
962
|
+
"div",
|
|
963
|
+
{
|
|
964
|
+
ref,
|
|
965
|
+
className: cn(
|
|
966
|
+
"floating-popup",
|
|
967
|
+
visible && "floating-popup-visible",
|
|
968
|
+
pos.direction === "above" ? "floating-popup-above" : "floating-popup-below",
|
|
969
|
+
className
|
|
970
|
+
),
|
|
971
|
+
style: {
|
|
972
|
+
position: "fixed",
|
|
973
|
+
top: pos.top,
|
|
974
|
+
left: pos.left,
|
|
975
|
+
zIndex: 50,
|
|
976
|
+
width: 320
|
|
977
|
+
},
|
|
978
|
+
...props,
|
|
979
|
+
children: [
|
|
980
|
+
/* @__PURE__ */ jsxs11("div", { className: "flex items-center justify-between mb-2", children: [
|
|
981
|
+
title && /* @__PURE__ */ jsx20("span", { className: "text-sm font-semibold tracking-tight", children: title }),
|
|
982
|
+
/* @__PURE__ */ jsx20(
|
|
983
|
+
Button,
|
|
984
|
+
{
|
|
985
|
+
variant: "ghost",
|
|
986
|
+
size: "icon",
|
|
987
|
+
className: "h-6 w-6 rounded-full ml-auto",
|
|
988
|
+
onClick: onClose,
|
|
989
|
+
"aria-label": "Close popup",
|
|
990
|
+
children: /* @__PURE__ */ jsx20(X2, { className: "h-3.5 w-3.5" })
|
|
991
|
+
}
|
|
992
|
+
)
|
|
993
|
+
] }),
|
|
994
|
+
/* @__PURE__ */ jsx20("div", { className: "text-sm text-muted-foreground", children })
|
|
995
|
+
]
|
|
996
|
+
}
|
|
997
|
+
);
|
|
998
|
+
}
|
|
999
|
+
);
|
|
1000
|
+
FloatingPopup.displayName = "FloatingPopup";
|
|
954
1001
|
export {
|
|
955
1002
|
ArrowButton,
|
|
956
1003
|
Badge,
|
|
@@ -964,11 +1011,13 @@ export {
|
|
|
964
1011
|
Checkbox,
|
|
965
1012
|
ChooseList,
|
|
966
1013
|
Counter,
|
|
1014
|
+
FloatingPopup,
|
|
967
1015
|
HighlightText,
|
|
968
1016
|
Input,
|
|
969
1017
|
Popup,
|
|
970
1018
|
ProgressBar,
|
|
971
1019
|
Separator,
|
|
1020
|
+
Sidebar,
|
|
972
1021
|
StatusBar,
|
|
973
1022
|
Switch,
|
|
974
1023
|
Table,
|
|
@@ -983,6 +1032,5 @@ export {
|
|
|
983
1032
|
TabsTrigger,
|
|
984
1033
|
ThemeToggle,
|
|
985
1034
|
Tooltip,
|
|
986
|
-
WeekViewCalendar,
|
|
987
1035
|
cn
|
|
988
1036
|
};
|
|
@@ -148,54 +148,63 @@ Shows a colored dot next to a label. Children are shown after a divider.
|
|
|
148
148
|
|
|
149
149
|
---
|
|
150
150
|
|
|
151
|
-
##
|
|
151
|
+
## Sidebar
|
|
152
152
|
|
|
153
153
|
```tsx
|
|
154
|
-
import {
|
|
154
|
+
import { Sidebar } from "hqui";
|
|
155
155
|
```
|
|
156
156
|
|
|
157
157
|
**Props:**
|
|
158
158
|
|
|
159
|
-
| Prop
|
|
160
|
-
|
|
|
161
|
-
| `
|
|
162
|
-
| `
|
|
163
|
-
| `
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
color: string; // any CSS color
|
|
175
|
-
}
|
|
159
|
+
| Prop | Type | Default |
|
|
160
|
+
| ---------- | ------------------ | -------- |
|
|
161
|
+
| `side` | `'left'` `'right'` | required |
|
|
162
|
+
| `open` | `boolean` | required |
|
|
163
|
+
| `onToggle` | `() => void` | required |
|
|
164
|
+
| `width` | `number` | `240` |
|
|
165
|
+
|
|
166
|
+
A VSCode-style side panel. When closed, a small toggle button stays visible at the edge so the user can reopen it. The panel slides in and out with a smooth animation.
|
|
167
|
+
|
|
168
|
+
```tsx
|
|
169
|
+
const [open, setOpen] = useState(true);
|
|
170
|
+
|
|
171
|
+
<Sidebar side="left" open={open} onToggle={() => setOpen(!open)}>
|
|
172
|
+
<p>Panel content here</p>
|
|
173
|
+
</Sidebar>;
|
|
176
174
|
```
|
|
177
175
|
|
|
176
|
+
---
|
|
177
|
+
|
|
178
|
+
## FloatingPopup
|
|
179
|
+
|
|
178
180
|
```tsx
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
181
|
+
import { FloatingPopup } from "hqui";
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
**Props:**
|
|
185
|
+
|
|
186
|
+
| Prop | Type | Default |
|
|
187
|
+
| ----------- | -------------------------------- | -------- |
|
|
188
|
+
| `open` | `boolean` | `false` |
|
|
189
|
+
| `onClose` | `() => void` | required |
|
|
190
|
+
| `anchorRef` | `RefObject<HTMLElement \| null>` | required |
|
|
191
|
+
| `title` | `ReactNode` | none |
|
|
192
|
+
| `gap` | `number` | `8` |
|
|
193
|
+
|
|
194
|
+
A non-modal floating popup anchored to a trigger element. It does not dim the background. It positions itself below the anchor by default; if there is not enough viewport space below, it flips above. Press Escape to close.
|
|
195
|
+
|
|
196
|
+
```tsx
|
|
197
|
+
const ref = useRef<HTMLButtonElement>(null);
|
|
198
|
+
const [open, setOpen] = useState(false);
|
|
199
|
+
|
|
200
|
+
<Button ref={ref} onClick={() => setOpen(true)}>Show Info</Button>
|
|
201
|
+
|
|
202
|
+
<FloatingPopup
|
|
203
|
+
open={open}
|
|
204
|
+
onClose={() => setOpen(false)}
|
|
205
|
+
anchorRef={ref}
|
|
206
|
+
title="Details"
|
|
207
|
+
>
|
|
208
|
+
<p>This popup does not block the background.</p>
|
|
209
|
+
</FloatingPopup>
|
|
201
210
|
```
|
package/package.json
CHANGED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import React, {
|
|
2
|
+
HTMLAttributes,
|
|
3
|
+
forwardRef,
|
|
4
|
+
useEffect,
|
|
5
|
+
useLayoutEffect,
|
|
6
|
+
useState,
|
|
7
|
+
useCallback,
|
|
8
|
+
} from "react";
|
|
9
|
+
import { X } from "lucide-react";
|
|
10
|
+
import { Button } from "../Button";
|
|
11
|
+
import { cn } from "../../lib/cn";
|
|
12
|
+
|
|
13
|
+
export interface FloatingPopupProps extends Omit<
|
|
14
|
+
HTMLAttributes<HTMLDivElement>,
|
|
15
|
+
"title"
|
|
16
|
+
> {
|
|
17
|
+
open: boolean;
|
|
18
|
+
onClose: () => void;
|
|
19
|
+
anchorRef: React.RefObject<HTMLElement | null>;
|
|
20
|
+
title?: React.ReactNode;
|
|
21
|
+
gap?: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface Position {
|
|
25
|
+
top: number;
|
|
26
|
+
left: number;
|
|
27
|
+
direction: "below" | "above";
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const FloatingPopup = forwardRef<HTMLDivElement, FloatingPopupProps>(
|
|
31
|
+
(
|
|
32
|
+
{ className, open, onClose, anchorRef, title, gap = 8, children, ...props },
|
|
33
|
+
ref,
|
|
34
|
+
) => {
|
|
35
|
+
const [pos, setPos] = useState<Position | null>(null);
|
|
36
|
+
const [visible, setVisible] = useState(false);
|
|
37
|
+
|
|
38
|
+
const calculate = useCallback(() => {
|
|
39
|
+
if (!anchorRef.current) return;
|
|
40
|
+
const rect = anchorRef.current.getBoundingClientRect();
|
|
41
|
+
const popupHeight = 200;
|
|
42
|
+
const popupWidth = 320;
|
|
43
|
+
|
|
44
|
+
const spaceBelow = window.innerHeight - rect.bottom;
|
|
45
|
+
const direction: "below" | "above" =
|
|
46
|
+
spaceBelow >= popupHeight + gap ? "below" : "above";
|
|
47
|
+
|
|
48
|
+
let top: number;
|
|
49
|
+
if (direction === "below") {
|
|
50
|
+
top = rect.bottom + gap;
|
|
51
|
+
} else {
|
|
52
|
+
top = rect.top - popupHeight - gap;
|
|
53
|
+
if (top < 8) top = 8;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
let left = rect.left + rect.width / 2 - popupWidth / 2;
|
|
57
|
+
if (left < 8) left = 8;
|
|
58
|
+
if (left + popupWidth > window.innerWidth - 8) {
|
|
59
|
+
left = window.innerWidth - popupWidth - 8;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
setPos({ top, left, direction });
|
|
63
|
+
}, [anchorRef, gap]);
|
|
64
|
+
|
|
65
|
+
useLayoutEffect(() => {
|
|
66
|
+
if (open) {
|
|
67
|
+
calculate();
|
|
68
|
+
requestAnimationFrame(() => setVisible(true));
|
|
69
|
+
} else {
|
|
70
|
+
setVisible(false);
|
|
71
|
+
setPos(null);
|
|
72
|
+
}
|
|
73
|
+
}, [open, calculate]);
|
|
74
|
+
|
|
75
|
+
useEffect(() => {
|
|
76
|
+
if (!open) return;
|
|
77
|
+
const onResize = () => calculate();
|
|
78
|
+
const onScroll = () => calculate();
|
|
79
|
+
window.addEventListener("resize", onResize);
|
|
80
|
+
window.addEventListener("scroll", onScroll, true);
|
|
81
|
+
return () => {
|
|
82
|
+
window.removeEventListener("resize", onResize);
|
|
83
|
+
window.removeEventListener("scroll", onScroll, true);
|
|
84
|
+
};
|
|
85
|
+
}, [open, calculate]);
|
|
86
|
+
|
|
87
|
+
useEffect(() => {
|
|
88
|
+
if (!open) return;
|
|
89
|
+
const handleEsc = (e: KeyboardEvent) => {
|
|
90
|
+
if (e.key === "Escape") onClose();
|
|
91
|
+
};
|
|
92
|
+
document.addEventListener("keydown", handleEsc);
|
|
93
|
+
return () => document.removeEventListener("keydown", handleEsc);
|
|
94
|
+
}, [open, onClose]);
|
|
95
|
+
|
|
96
|
+
if (!open || !pos) return null;
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
<div
|
|
100
|
+
ref={ref}
|
|
101
|
+
className={cn(
|
|
102
|
+
"floating-popup",
|
|
103
|
+
visible && "floating-popup-visible",
|
|
104
|
+
pos.direction === "above"
|
|
105
|
+
? "floating-popup-above"
|
|
106
|
+
: "floating-popup-below",
|
|
107
|
+
className,
|
|
108
|
+
)}
|
|
109
|
+
style={{
|
|
110
|
+
position: "fixed",
|
|
111
|
+
top: pos.top,
|
|
112
|
+
left: pos.left,
|
|
113
|
+
zIndex: 50,
|
|
114
|
+
width: 320,
|
|
115
|
+
}}
|
|
116
|
+
{...props}
|
|
117
|
+
>
|
|
118
|
+
<div className="flex items-center justify-between mb-2">
|
|
119
|
+
{title && (
|
|
120
|
+
<span className="text-sm font-semibold tracking-tight">
|
|
121
|
+
{title}
|
|
122
|
+
</span>
|
|
123
|
+
)}
|
|
124
|
+
<Button
|
|
125
|
+
variant="ghost"
|
|
126
|
+
size="icon"
|
|
127
|
+
className="h-6 w-6 rounded-full ml-auto"
|
|
128
|
+
onClick={onClose}
|
|
129
|
+
aria-label="Close popup"
|
|
130
|
+
>
|
|
131
|
+
<X className="h-3.5 w-3.5" />
|
|
132
|
+
</Button>
|
|
133
|
+
</div>
|
|
134
|
+
<div className="text-sm text-muted-foreground">{children}</div>
|
|
135
|
+
</div>
|
|
136
|
+
);
|
|
137
|
+
},
|
|
138
|
+
);
|
|
139
|
+
FloatingPopup.displayName = "FloatingPopup";
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import React, { HTMLAttributes, forwardRef } from "react";
|
|
2
|
+
import { ChevronLeft, ChevronRight } from "lucide-react";
|
|
3
|
+
import { cn } from "../../lib/cn";
|
|
4
|
+
|
|
5
|
+
export interface SidebarProps extends HTMLAttributes<HTMLDivElement> {
|
|
6
|
+
side: "left" | "right";
|
|
7
|
+
open: boolean;
|
|
8
|
+
onToggle: () => void;
|
|
9
|
+
width?: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const Sidebar = forwardRef<HTMLDivElement, SidebarProps>(
|
|
13
|
+
(
|
|
14
|
+
{ className, side, open, onToggle, width = 240, children, style, ...props },
|
|
15
|
+
ref,
|
|
16
|
+
) => {
|
|
17
|
+
const isLeft = side === "left";
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<div
|
|
21
|
+
ref={ref}
|
|
22
|
+
className={cn("sidebar-root", `sidebar-${side}`, className)}
|
|
23
|
+
style={
|
|
24
|
+
{
|
|
25
|
+
"--sidebar-w": `${width}px`,
|
|
26
|
+
...style,
|
|
27
|
+
} as React.CSSProperties
|
|
28
|
+
}
|
|
29
|
+
{...props}
|
|
30
|
+
>
|
|
31
|
+
{/* Panel */}
|
|
32
|
+
<div
|
|
33
|
+
className={cn(
|
|
34
|
+
"sidebar-panel",
|
|
35
|
+
open ? "sidebar-panel-open" : "sidebar-panel-closed",
|
|
36
|
+
)}
|
|
37
|
+
>
|
|
38
|
+
<div className="sidebar-content">{children}</div>
|
|
39
|
+
</div>
|
|
40
|
+
|
|
41
|
+
{/* Toggle button — always visible */}
|
|
42
|
+
<button
|
|
43
|
+
type="button"
|
|
44
|
+
onClick={onToggle}
|
|
45
|
+
className="sidebar-toggle"
|
|
46
|
+
aria-label={open ? `Hide ${side} sidebar` : `Show ${side} sidebar`}
|
|
47
|
+
>
|
|
48
|
+
{isLeft ? (
|
|
49
|
+
open ? (
|
|
50
|
+
<ChevronLeft className="h-4 w-4" />
|
|
51
|
+
) : (
|
|
52
|
+
<ChevronRight className="h-4 w-4" />
|
|
53
|
+
)
|
|
54
|
+
) : open ? (
|
|
55
|
+
<ChevronRight className="h-4 w-4" />
|
|
56
|
+
) : (
|
|
57
|
+
<ChevronLeft className="h-4 w-4" />
|
|
58
|
+
)}
|
|
59
|
+
</button>
|
|
60
|
+
</div>
|
|
61
|
+
);
|
|
62
|
+
},
|
|
63
|
+
);
|
|
64
|
+
Sidebar.displayName = "Sidebar";
|
package/src/index.css
CHANGED
|
@@ -117,3 +117,94 @@
|
|
|
117
117
|
transform: scale(1);
|
|
118
118
|
}
|
|
119
119
|
}
|
|
120
|
+
|
|
121
|
+
/* ── Sidebar ── */
|
|
122
|
+
.sidebar-root {
|
|
123
|
+
position: relative;
|
|
124
|
+
display: flex;
|
|
125
|
+
flex-shrink: 0;
|
|
126
|
+
height: 100%;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.sidebar-left {
|
|
130
|
+
flex-direction: row;
|
|
131
|
+
}
|
|
132
|
+
.sidebar-right {
|
|
133
|
+
flex-direction: row-reverse;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
.sidebar-panel {
|
|
137
|
+
width: var(--sidebar-w, 240px);
|
|
138
|
+
overflow: hidden;
|
|
139
|
+
background: hsl(var(--card));
|
|
140
|
+
border-right: 1px solid hsl(var(--border));
|
|
141
|
+
transition:
|
|
142
|
+
width 0.2s ease-in-out,
|
|
143
|
+
opacity 0.2s ease-in-out;
|
|
144
|
+
}
|
|
145
|
+
.sidebar-right .sidebar-panel {
|
|
146
|
+
border-right: none;
|
|
147
|
+
border-left: 1px solid hsl(var(--border));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
.sidebar-panel-open {
|
|
151
|
+
opacity: 1;
|
|
152
|
+
}
|
|
153
|
+
.sidebar-panel-closed {
|
|
154
|
+
width: 0;
|
|
155
|
+
opacity: 0;
|
|
156
|
+
border-width: 0;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
.sidebar-content {
|
|
160
|
+
width: var(--sidebar-w, 240px);
|
|
161
|
+
height: 100%;
|
|
162
|
+
overflow-y: auto;
|
|
163
|
+
padding: 0.5rem;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
.sidebar-toggle {
|
|
167
|
+
display: flex;
|
|
168
|
+
align-items: center;
|
|
169
|
+
justify-content: center;
|
|
170
|
+
width: 24px;
|
|
171
|
+
flex-shrink: 0;
|
|
172
|
+
cursor: pointer;
|
|
173
|
+
border: none;
|
|
174
|
+
background: hsl(var(--muted));
|
|
175
|
+
color: hsl(var(--muted-foreground));
|
|
176
|
+
transition:
|
|
177
|
+
background 0.15s,
|
|
178
|
+
color 0.15s;
|
|
179
|
+
}
|
|
180
|
+
.sidebar-toggle:hover {
|
|
181
|
+
background: hsl(var(--accent));
|
|
182
|
+
color: hsl(var(--accent-foreground));
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/* ── Floating Popup ── */
|
|
186
|
+
.floating-popup {
|
|
187
|
+
background: hsl(var(--card));
|
|
188
|
+
border: 1px solid hsl(var(--border));
|
|
189
|
+
border-radius: 0.75rem;
|
|
190
|
+
padding: 0.75rem 1rem;
|
|
191
|
+
box-shadow:
|
|
192
|
+
0 4px 24px rgba(0, 0, 0, 0.12),
|
|
193
|
+
0 1px 4px rgba(0, 0, 0, 0.08);
|
|
194
|
+
opacity: 0;
|
|
195
|
+
transition:
|
|
196
|
+
opacity 0.15s ease-out,
|
|
197
|
+
transform 0.15s ease-out;
|
|
198
|
+
pointer-events: none;
|
|
199
|
+
}
|
|
200
|
+
.floating-popup-below {
|
|
201
|
+
transform: translateY(-6px);
|
|
202
|
+
}
|
|
203
|
+
.floating-popup-above {
|
|
204
|
+
transform: translateY(6px);
|
|
205
|
+
}
|
|
206
|
+
.floating-popup-visible {
|
|
207
|
+
opacity: 1;
|
|
208
|
+
transform: translateY(0);
|
|
209
|
+
pointer-events: auto;
|
|
210
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -22,4 +22,5 @@ export * from "./components/extended/ArrowButton";
|
|
|
22
22
|
export * from "./components/extended/ChooseList";
|
|
23
23
|
export * from "./components/extended/Popup";
|
|
24
24
|
export * from "./components/extended/StatusBar";
|
|
25
|
-
export * from "./components/extended/
|
|
25
|
+
export * from "./components/extended/Sidebar";
|
|
26
|
+
export * from "./components/extended/FloatingPopup";
|