wip moving menu nav buttons in widget menu

This commit is contained in:
2026-09-11 14:45:39 -07:00
parent 4914b64fe2
commit 0c8820d109
3 changed files with 253 additions and 105 deletions
@@ -31,20 +31,23 @@ export function SettingsButton() {
const cancelEdit = useDashboardStore(state => state.cancelEdit); const cancelEdit = useDashboardStore(state => state.cancelEdit);
const addWidget = useDashboardStore(state => state.addWidget); const addWidget = useDashboardStore(state => state.addWidget);
const recipe = useRecipe({key: "glassTab"}); const glassTabRecipe = useRecipe({key: "glassTab"});
const tabStyles = recipe(); const settingsButtonRecipe = useRecipe({key: "settingsButton"});
const tabStyles = glassTabRecipe();
const settingsButtonStyles = settingsButtonRecipe();
const [canScrollLeft, setCanScrollLeft] = useState(false);
const [canScrollRight, setCanScrollRight] = useState(false);
// Pull the glass container styles for the dropdown and modal // Pull the glass container styles for the dropdown and modal
const panelRecipe = useSlotRecipe({key: "glassPanel"}); const panelRecipe = useSlotRecipe({key: "glassPanel"});
const panelStyles = panelRecipe({layout: "dropdown"}); const panelStyles = panelRecipe({layout: "dropdown"});
const modalStyles = panelRecipe({layout: "popover"}); const modalStyles = panelRecipe({layout: "popover", leftScroll: canScrollLeft ? "visible" : "hidden", rightScroll: canScrollRight ? "visible" : "hidden"});
const [isLibraryOpen, setIsLibraryOpen] = useState(false); const [isLibraryOpen, setIsLibraryOpen] = useState(false);
// ⬅️ ➡️ This is the callbacks and scroll control to add the chevron arrows in the tab list // ⬅️ ➡️ This is the callbacks and scroll control to add the chevron arrows in the tab list
const tabsRef = useRef(null); const tabsRef = useRef(null);
const [canScrollLeft, setCanScrollLeft] = useState(false);
const [canScrollRight, setCanScrollRight] = useState(false);
const checkScroll = useCallback(() => { const checkScroll = useCallback(() => {
if (tabsRef.current) { if (tabsRef.current) {
@@ -92,7 +95,7 @@ export function SettingsButton() {
<> <>
{isEditing ? ( {isEditing ? (
// 📝 EDIT MODE: The 3-Pill Layout // 📝 EDIT MODE: The 3-Pill Layout
<HStack gap={2}> <HStack css={settingsButtonStyles.root}>
{/* 1. The Add Widget Button */} {/* 1. The Add Widget Button */}
<Box <Box
as="button" as="button"
@@ -137,7 +140,7 @@ export function SettingsButton() {
<Menu.Root positioning={{getAnchorRect}}> <Menu.Root positioning={{getAnchorRect}}>
<Menu.Trigger asChild> <Menu.Trigger asChild>
<Box ref={menuRef} as="button" css={tabStyles}> <Box ref={menuRef} as="button" css={tabStyles}>
{/* size increased to 24 to match tabs. flexShrink: 0 is CRITICAL */} {/* settings icon size set to 24 to match tabs. flexShrink 0 is CRITICAL */}
<Settings size={24} style={{flexShrink: 0}}/> <Settings size={24} style={{flexShrink: 0}}/>
<Box as="span">Dashboard Settings</Box> <Box as="span">Dashboard Settings</Box>
</Box> </Box>
@@ -174,27 +177,24 @@ export function SettingsButton() {
<Dialog.Root <Dialog.Root
open={isLibraryOpen} open={isLibraryOpen}
onOpenChange={(e) => setIsLibraryOpen(e.open)} onOpenChange={(e) => setIsLibraryOpen(e.open)}
size="xl"
placement="center"
closeOnInteractOutside={true} closeOnInteractOutside={true}
closeOnEscapeKeyDown={true} closeOnEscapeKeyDown={true}
size="xl"
placement="center"
> >
{/* Soft blur can be added to the background behind the modal */} {/* Soft blur can be added to the background behind the modal */}
{/*<Dialog.Backdrop bg="blackAlpha.100" backdropFilter="blur(2px)"/>*/} {/*<Dialog.Backdrop bg="blackAlpha.100" backdropFilter="blur(2px)"/>*/}
<Dialog.Positioner> <Dialog.Positioner>
<Dialog.Content css={modalStyles.root}>
<Dialog.Content css={modalStyles.root} overflow="hidden" maxW="60%" minH="80%" p={0}> <Dialog.Header>
<Dialog.Title>Widgets Shop</Dialog.Title>
<Dialog.Header bg="transparent" color={{base: "gray.800", _dark: "white"}} py={4}
borderColor={{base: "gray.200", _dark: "whiteAlpha.200"}}>
<Dialog.Title fontSize="xl" fontWeight="bold">Widgets Shop</Dialog.Title>
</Dialog.Header> </Dialog.Header>
<Dialog.Body p={0}> <Dialog.Body>
<Tabs.Root defaultValue={categories[0]} variant="unstyled"> <Tabs.Root defaultValue={categories[0]} variant="unstyled">
<Box position="relative" display="flex" mx="auto" maxWidth="100%" mx={5} mt={4} mb={6}> {/* HORIZONTAL SCROLLABLE MENU OF WIDGET CATEGORIES */}
<Box position="relative" display="flex" maxWidth="100%" mx={5} mt={4} mb={6}>
{/* LEFT GLASS CRESCENT */} {/* left glass crescent */}
{canScrollLeft && ( {canScrollLeft && (
<Flex <Flex
position="absolute" position="absolute"
@@ -224,55 +224,20 @@ export function SettingsButton() {
<ChevronLeft size={18} style={{ marginLeft: "-30px" }}/> <ChevronLeft size={18} style={{ marginLeft: "-30px" }}/>
</Flex> </Flex>
)} )}
<Tabs.List
ref={tabsRef} {/* the categories */}
onScroll={checkScroll} <Tabs.List ref={tabsRef} onScroll={checkScroll}
bg={{base: "rgba(0, 0, 0, 0.04)", _dark: "rgba(255, 255, 255, 0.05)"}} // For desktop users (not on trackpad) to scroll
p={1} onWheel={(e) => { if (e.currentTarget) e.currentTarget.scrollLeft += e.deltaY; }}
borderRadius="full"
display="flex"
flexWrap="nowrap"
overflowX="auto"
maxWidth="100%"
border="1px solid"
borderColor={{base: "blackAlpha.50", _dark: "whiteAlpha.100"}}
css={{
scrollbarWidth: "none",
"&::-webkit-scrollbar": {display: "none"},
}}
// This is for desktop users not on trackpad to scroll the
onWheel={(e) => {
if (e.currentTarget) {
e.currentTarget.scrollLeft += e.deltaY;
}
}}
> >
{categories.map(cat => ( {categories.map(cat => (
<Tabs.Trigger <Tabs.Trigger key={cat} value={cat}>
key={cat}
value={cat}
px={5}
py={2}
borderRadius="full"
whiteSpace="nowrap"
flexShrink={0}
color={{base: "gray.500", _dark: "gray.400"}}
fontWeight="medium"
transition="background-color 0.2s, color 0.2s, box-shadow 0.2s"
_hover={{
color: {base: "gray.800", _dark: "gray.200"}
}}
_selected={{
bg: {base: "white", _dark: "rgba(255, 255, 255, 0.12)"},
color: {base: "blue.600", _dark: "blue.200"},
boxShadow: "sm",
fontWeight: "bold",
}}
>
{cat} {cat}
</Tabs.Trigger> </Tabs.Trigger>
))} ))}
</Tabs.List> </Tabs.List>
{/* right glass crescent */}
{canScrollRight && ( {canScrollRight && (
<Flex <Flex
position="absolute" position="absolute"
@@ -302,8 +267,9 @@ export function SettingsButton() {
<ChevronRight size={18} style={{ marginRight: "-30px" }}/> <ChevronRight size={18} style={{ marginRight: "-30px" }}/>
</Flex> </Flex>
)} )}
</Box> </Box>
{/* THE WIDGET OPTIONS W/IN EACH CATEGORY */}
{categories.map(cat => ( {categories.map(cat => (
<Tabs.Content key={cat} value={cat} p={6}> <Tabs.Content key={cat} value={cat} p={6}>
<Grid templateColumns="repeat(auto-fill, minmax(300px, 1fr))" gap={4}> <Grid templateColumns="repeat(auto-fill, minmax(300px, 1fr))" gap={4}>
+5 -24
View File
@@ -68,28 +68,21 @@ export default function MyDashboard() {
return ( return (
<> <>
<Box css={styles.header}> <Box css={styles.header}>
<Text color="titleColor" fontWeight="semibold" fontSize="md" isTruncated> <Text>
{registryEntry.name} {registryEntry.name}
</Text> </Text>
{/* WIDGET SETTINGS BUTTON (ELLIPSES) */} {/* WIDGET SETTINGS BUTTON (ELLIPSES) */}
{SettingsUI ? ( {SettingsUI && (
<Popover.Root positioning={{placement: "bottom-end"}}> <Popover.Root positioning={{placement: "bottom-end"}}>
<Popover.Trigger asChild> <Popover.Trigger asChild>
<IconButton <IconButton size="xs" variant="ghost" aria-label="Widget Settings">
size="xs"
variant="ghost"
color="gray.400"
borderRadius="xl"
_hover={{color: "white", bg: "gray.700"}}
aria-label="Widget Settings"
>
<MoreVertical size={16}/> <MoreVertical size={16}/>
</IconButton> </IconButton>
</Popover.Trigger> </Popover.Trigger>
{/* NEW: The Positioner lifts the content out of the Flexbox flow */} {/* NEW: The Positioner lifts the content out of the Flexbox flow */}
<Popover.Positioner zIndex={10}> <Popover.Positioner zIndex={1}>
<Popover.Content css={popoverStyles.root}> <Popover.Content css={popoverStyles.root}>
<Suspense fallback={<Spinner size="sm"/>}> <Suspense fallback={<Spinner size="sm"/>}>
<SettingsUI <SettingsUI
@@ -100,8 +93,6 @@ export default function MyDashboard() {
</Popover.Content> </Popover.Content>
</Popover.Positioner> </Popover.Positioner>
</Popover.Root> </Popover.Root>
) : (
<Box w="20px" h="20px"/>
)} )}
</Box> </Box>
@@ -131,8 +122,8 @@ export default function MyDashboard() {
rowHeight={30} rowHeight={30}
isDraggable={isEditing} // Only allows dragging when Settings tools toggle this isDraggable={isEditing} // Only allows dragging when Settings tools toggle this
isResizable={false} // Double lock-down isResizable={false} // Double lock-down
onLayoutChange={(newLayout) => updateLayoutPositions(newLayout)} // Fires when user drops a widget
compactType="vertical" // Auto-packs widgets to the top compactType="vertical" // Auto-packs widgets to the top
onLayoutChange={(newLayout) => updateLayoutPositions(newLayout)} // Fires when user drops a widget
> >
{/* 🎨 Paint the Widgets */} {/* 🎨 Paint the Widgets */}
{widgets.map(widget => ( {widgets.map(widget => (
@@ -151,17 +142,7 @@ export default function MyDashboard() {
{isEditing && ( {isEditing && (
<IconButton <IconButton
aria-label="Remove widget" aria-label="Remove widget"
position="absolute"
top={2}
right={2}
size="xs" size="xs"
bg="white"
color="black"
border="1px solid"
borderRadius="xl"
borderColor="gray.200"
_hover={{bg: "red.50", color: "red.500", borderColor: "red.200"}}
zIndex={2}
onClick={() => removeWidget(widget.id)} onClick={() => removeWidget(widget.id)}
onMouseDown={(e) => e.stopPropagation()} // 🚫 Stops RGL from grabbing the widget onMouseDown={(e) => e.stopPropagation()} // 🚫 Stops RGL from grabbing the widget
onTouchStart={(e) => e.stopPropagation()} // 🚫 Same protection for mobile touches onTouchStart={(e) => e.stopPropagation()} // 🚫 Same protection for mobile touches
+219 -18
View File
@@ -97,9 +97,11 @@ const sidebarRecipe = defineSlotRecipe({
_hover: { _hover: {
bg: "rgba(255, 255, 255, 0.2)", bg: "rgba(255, 255, 255, 0.2)",
color: "{text}", color: "{text}",
fontWeight: "500",
"& svg": { "& svg": {
strokeWidth: "3" strokeWidth: "3"
},
"& span": {
fontWeight: "700",
} }
}, },
@@ -119,9 +121,11 @@ const sidebarRecipe = defineSlotRecipe({
_hover: { _hover: {
bg: "{MainBackground}", bg: "{MainBackground}",
color: "{textInverted}", color: "{textInverted}",
fontWeight: "normal",
"& svg": { "& svg": {
strokeWidth: "2" // Keeps the active icon at normal thickness on hover strokeWidth: "2" // Keeps the active icon at normal thickness on hover
},
"& span": {
fontWeight: "500",
} }
}, },
@@ -181,7 +185,7 @@ const sidebarRecipe = defineSlotRecipe({
whiteSpace: "nowrap", whiteSpace: "nowrap",
overflow: "hidden", overflow: "hidden",
textOverflow: "clip", textOverflow: "clip",
fontWeight: "500" fontWeight: "500",
}, },
collapse_button: { collapse_button: {
"& svg": {flexShrink: "0"}, "& svg": {flexShrink: "0"},
@@ -244,14 +248,7 @@ const dashboardRecipe = defineRecipe({
} }
}); });
const canvasRecipe = defineRecipe({ // CONSOLIDATED ✔
base: {
bg: "{transparent}",
zIndex: "-1",
color: "{text}"
}
});
const homeRecipe = defineSlotRecipe({ const homeRecipe = defineSlotRecipe({
slots: ["root"], slots: ["root"],
base: { base: {
@@ -291,6 +288,30 @@ const homeRecipe = defineSlotRecipe({
}, },
}) })
const settingsButtonRecipe = defineSlotRecipe({
slots: ["root", "dialog"],
base: {
root: {
gap: 2,
"& > button": {
}
},
dialog: {
}
}
})
// const canvasRecipe = defineRecipe({
// base: {
// bg: "{transparent}",
// zIndex: "-1",
// color: "{text}"
// }
// });
// const widgetRecipe = defineSlotRecipe({ // const widgetRecipe = defineSlotRecipe({
// slots: ["root", "header", "canvas"], // slots: ["root", "header", "canvas"],
// base: { // base: {
@@ -329,6 +350,9 @@ const homeRecipe = defineSlotRecipe({
// } // }
// }); // });
// USED IN COMPONENTS:
// * Settings Button
// * My Dashboard
const glassPanelRecipe = defineSlotRecipe({ const glassPanelRecipe = defineSlotRecipe({
slots: ["root", "header", "body"], slots: ["root", "header", "body"],
@@ -352,26 +376,202 @@ const glassPanelRecipe = defineSlotRecipe({
variants: { variants: {
layout: { layout: {
widget: { widget: {
root: {width: "100%", height: "100%", borderRadius: "3xl"}, root: {
header: {display: "flex", justifyContent: "space-between", alignItems: "center", px: 3, pt: 2, pb: 0}, width: "100%",
body: {flex: "1", overflow: "hidden", position: "relative", px: 1, pb: 1, pt: 0} height: "100%",
borderRadius: "3xl",
"& > button": {
position: "absolute",
top: 2,
right: 2,
// size="xs",
bg: "white",
color: "black",
border: "1px solid",
borderRadius: "xl",
borderColor: "gray.200",
_hover: {
bg: "red.50",
color: "red.500",
borderColor: "red.200"
},
zIndex: 2,
}
},
header: {
display: "flex",
justifyContent: "space-between",
alignItems: "center",
px: 3,
pt: 2,
pb: 0,
"& > p": {
color: "titleColor",
fontWeight: "500",
fontSize: "16px",
// the following three specifications are equivalent to Chakra's 'truncate' prop
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "clip",
},
"& > button": {
borderRadius: "xl",
_hover: {
color: "white",
bg: "gray.500",
},
}
},
body: {
flex: "1",
overflow: "hidden",
position: "relative",
px: 1,
pb: 1,
pt: 0
}
}, },
dropdown: { dropdown: {
root: {borderRadius: "xl", p: 1} root: {
borderRadius: "xl",
p: 1
}
// (Header and body slots are ignored here because dropdowns don't use them) // (Header and body slots are ignored here because dropdowns don't use them)
}, },
popover: { popover: {
root: { root: {
borderRadius: "2xl", borderRadius: "2xl",
p: 4, p: 0,
boxShadow: "xl", boxShadow: "xl",
minW: "240px", minW: "240px",
maxW: "60%",
minH: "80%",
overflow: "hidden",
// OVERRIDES: Make the background 85-90% opaque instead of 40% // OVERRIDES: Make the background 85-90% opaque instead of 40%
bg: {base: "rgba(255, 255, 255, 0.75)", _dark: "rgba(20, 22, 25, 0.8)"}, bg: {
base: "rgba(255, 255, 255, 0.75)",
_dark: "rgba(20, 22, 25, 0.8)"
},
// OVERRIDES: Increase the blur just to smooth out any stubborn gridlines // OVERRIDES: Increase the blur just to smooth out any stubborn gridlines
backdropFilter: "blur(15px)", backdropFilter: "blur(15px)",
WebkitBackdropFilter: "blur(15px)", WebkitBackdropFilter: "blur(15px)",
// dialog header
"& > div.chakra-dialog__header": {
bg: "transparent",
color: {
base: "gray.800",
_dark: "white"
},
py: 4,
borderColor: {
base: "gray.200",
_dark: "whiteAlpha.200"
},
// title
"& h2": {
fontSize: "xl",
fontWeight: "bold"
},
},
// dialog body
"& > div.chakra-dialog__body": {
p: 0,
// everything below is specific to the widget menu found in the settings button component
// the horizontal widget categories menu
"& div.chakra-tabs__list": {
bg: {
base: "rgba(0, 0, 0, 0.04)",
_dark: "rgba(255, 255, 255, 0.05)"
},
p: 1,
borderRadius: "full",
display: "flex",
flexWrap: "nowrap",
overflowX: "auto",
maxW: "100%",
border: "1px solid",
borderColor: {
base: "blackAlpha.50",
_dark: "whiteAlpha.100"
},
scrollbarWidth: "none",
"&::-webkit-scrollbar": {display: "none"},
"& button.chakra-tabs__trigger": {
px: 5,
py: 2,
borderRadius: "full",
whiteSpace: "nowrap",
flexShrink: 0,
color: {
base: "gray.500",
_dark: "gray.400"
},
fontWeight: "medium",
transition: "background-color 0.2s, color 0.2s, box-shadow 0.2s",
_hover: {
color: {
base: "gray.800",
_dark: "gray.200"
},
},
_selected: {
bg: {
base: "white",
_dark: "rgba(255, 255, 255, 0.12)"
},
color: {
base: "blue.600",
_dark: "blue.200"
},
boxShadow: "sm",
fontWeight: "bold",
}
}
},
// "& div.chakra-tabs__root > div:first-child div > svg": {
// position: "absolute",
// top: "0",
// bottom: "0",
// w: "50px",
// align: "center",
// justify: "center",
// zIndex: 2,
// cursor: "pointer",
// bg: {
// base: "rgba(255, 255, 255, 0.7)",
// _dark: "rgba(30, 30, 30, 0.8)"
// },
// backdropFilter: "blur(1px)",
//
// color: {
// base: "gray.600",
// _dark: "gray.300"
// },
// transition: "background-color 0.2s",
// _hover: {
// color: {
// base: "blue.500",
// _dark: "blue.300"
// },
// bg: {
// base: "rgba(255, 255, 255, 0.9)",
// _dark: "rgba(40, 40, 40, 0.95)"
// },
// }
// }
}
} }
} }
}, },
@@ -554,7 +754,7 @@ const config = defineConfig({
}, },
recipes: { recipes: {
dashboard: dashboardRecipe, dashboard: dashboardRecipe,
canvas: canvasRecipe, // canvas: canvasRecipe,
glassTab: glassTabRecipe glassTab: glassTabRecipe
}, },
slotRecipes: { slotRecipes: {
@@ -563,6 +763,7 @@ const config = defineConfig({
// widget: widgetRecipe, // widget: widgetRecipe,
glassPanel: glassPanelRecipe, glassPanel: glassPanelRecipe,
home: homeRecipe, home: homeRecipe,
settingsButton: settingsButtonRecipe,
}, },
}, },
}); });