@forwardreach/saas-ui 0.4.0 → 0.5.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/CHANGELOG.md CHANGED
@@ -1,5 +1,26 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.5.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 523e143: Add `OverflowMenu` — a "⋯" icon-button that opens a dropdown of low-frequency,
8
+ whole-object actions, composed from the existing Radix `DropdownMenu` primitives.
9
+ Panel contents are composed with the exported `OverflowMenuItem` and
10
+ `OverflowMenuSeparator` aliases.
11
+
12
+ Menu-item highlight (hover/keyboard) now uses the translucent
13
+ `--ssui-overlay-hover` token instead of `--ssui-surface-muted`, and also styles
14
+ Radix's `data-highlighted` state. Solid `surface-muted` was invisible on themes
15
+ where it matches `surface-elevated` (the panel background); a translucent
16
+ overlay reads on any surface.
17
+
18
+ `DropdownMenuContent` (and `OverflowMenu`) also accept a new `portal` prop.
19
+ It defaults to `true`, preserving today's portal-to-`document.body` behavior;
20
+ pass `false` to render the panel inline next to the trigger, which keeps it
21
+ inside the trigger's DOM subtree — needed when the menu lives in a surface that
22
+ tracks focus, such as a click-to-edit editor that persists on blur.
23
+
3
24
  ## 0.4.0
4
25
 
5
26
  ### Minor Changes
@@ -10,7 +10,15 @@ export declare const DropdownMenuSubTrigger: React.ForwardRefExoticComponent<Omi
10
10
  inset?: boolean;
11
11
  } & React.RefAttributes<HTMLDivElement>>;
12
12
  export declare const DropdownMenuSubContent: React.ForwardRefExoticComponent<Omit<DropdownMenuPrimitive.DropdownMenuSubContentProps & React.RefAttributes<HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
13
- export declare const DropdownMenuContent: React.ForwardRefExoticComponent<Omit<DropdownMenuPrimitive.DropdownMenuContentProps & React.RefAttributes<HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
13
+ export declare const DropdownMenuContent: React.ForwardRefExoticComponent<Omit<DropdownMenuPrimitive.DropdownMenuContentProps & React.RefAttributes<HTMLDivElement>, "ref"> & {
14
+ /**
15
+ * Render the panel through a portal to `document.body` (the default). Pass
16
+ * `false` to render it inline, next to the trigger — useful when the menu
17
+ * lives inside a subtree that tracks focus (e.g. a click-to-edit editor that
18
+ * persists on blur) and must keep the open panel within its own DOM.
19
+ */
20
+ portal?: boolean;
21
+ } & React.RefAttributes<HTMLDivElement>>;
14
22
  export declare const DropdownMenuItem: React.ForwardRefExoticComponent<Omit<DropdownMenuPrimitive.DropdownMenuItemProps & React.RefAttributes<HTMLDivElement>, "ref"> & {
15
23
  inset?: boolean;
16
24
  } & React.RefAttributes<HTMLDivElement>>;
@@ -9,17 +9,20 @@ export const DropdownMenuGroup = DropdownMenuPrimitive.Group;
9
9
  export const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
10
10
  export const DropdownMenuSub = DropdownMenuPrimitive.Sub;
11
11
  export const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
12
- export const DropdownMenuSubTrigger = React.forwardRef(({ children, className, inset, ...props }, ref) => (_jsxs(DropdownMenuPrimitive.SubTrigger, { ref: ref, className: cn("flex cursor-default select-none items-center rounded-[var(--ssui-radius-sm)] px-2 py-1.5 text-sm text-[color:var(--ssui-text)] outline-none focus:bg-[color:var(--ssui-surface-muted)] data-[state=open]:bg-[color:var(--ssui-surface-muted)]", inset && "pl-8", className), ...props, children: [children, _jsx(ChevronRight, { className: "ml-auto size-4" })] })));
12
+ export const DropdownMenuSubTrigger = React.forwardRef(({ children, className, inset, ...props }, ref) => (_jsxs(DropdownMenuPrimitive.SubTrigger, { ref: ref, className: cn("flex cursor-default select-none items-center rounded-[var(--ssui-radius-sm)] px-2 py-1.5 text-sm text-[color:var(--ssui-text)] outline-none focus:bg-[color:var(--ssui-overlay-hover)] data-[highlighted]:bg-[color:var(--ssui-overlay-hover)] data-[state=open]:bg-[color:var(--ssui-overlay-hover)]", inset && "pl-8", className), ...props, children: [children, _jsx(ChevronRight, { className: "ml-auto size-4" })] })));
13
13
  DropdownMenuSubTrigger.displayName = DropdownMenuPrimitive.SubTrigger.displayName;
14
14
  export const DropdownMenuSubContent = React.forwardRef(({ className, ...props }, ref) => (_jsx(DropdownMenuPrimitive.SubContent, { ref: ref, className: cn("z-50 min-w-40 overflow-hidden rounded-[var(--ssui-radius)] border border-[color:var(--ssui-border)] bg-[color:var(--ssui-surface-elevated)] p-1 text-[color:var(--ssui-text)] shadow-[var(--ssui-shadow-md)]", className), ...props })));
15
15
  DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayName;
16
- export const DropdownMenuContent = React.forwardRef(({ className, sideOffset = 6, ...props }, ref) => (_jsx(DropdownMenuPrimitive.Portal, { children: _jsx(DropdownMenuPrimitive.Content, { ref: ref, sideOffset: sideOffset, className: cn("z-50 min-w-44 overflow-hidden rounded-[var(--ssui-radius)] border border-[color:var(--ssui-border)] bg-[color:var(--ssui-surface-elevated)] p-1 text-[color:var(--ssui-text)] shadow-[var(--ssui-shadow-md)]", className), ...props }) })));
16
+ export const DropdownMenuContent = React.forwardRef(({ className, portal = true, sideOffset = 6, ...props }, ref) => {
17
+ const content = (_jsx(DropdownMenuPrimitive.Content, { ref: ref, sideOffset: sideOffset, className: cn("z-50 min-w-44 overflow-hidden rounded-[var(--ssui-radius)] border border-[color:var(--ssui-border)] bg-[color:var(--ssui-surface-elevated)] p-1 text-[color:var(--ssui-text)] shadow-[var(--ssui-shadow-md)]", className), ...props }));
18
+ return portal ? _jsx(DropdownMenuPrimitive.Portal, { children: content }) : content;
19
+ });
17
20
  DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
18
- export const DropdownMenuItem = React.forwardRef(({ className, inset, ...props }, ref) => (_jsx(DropdownMenuPrimitive.Item, { ref: ref, className: cn("relative flex cursor-default select-none items-center gap-2 rounded-[var(--ssui-radius-sm)] px-2 py-1.5 text-sm outline-none transition-colors focus:bg-[color:var(--ssui-surface-muted)] data-[disabled]:pointer-events-none data-[disabled]:opacity-50", inset && "pl-8", className), ...props })));
21
+ export const DropdownMenuItem = React.forwardRef(({ className, inset, ...props }, ref) => (_jsx(DropdownMenuPrimitive.Item, { ref: ref, className: cn("relative flex cursor-default select-none items-center gap-2 rounded-[var(--ssui-radius-sm)] px-2 py-1.5 text-sm outline-none transition-colors focus:bg-[color:var(--ssui-overlay-hover)] data-[highlighted]:bg-[color:var(--ssui-overlay-hover)] data-[disabled]:pointer-events-none data-[disabled]:opacity-50", inset && "pl-8", className), ...props })));
19
22
  DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
20
- export const DropdownMenuCheckboxItem = React.forwardRef(({ children, className, checked, ...props }, ref) => (_jsxs(DropdownMenuPrimitive.CheckboxItem, { ref: ref, checked: checked, className: cn("relative flex cursor-default select-none items-center rounded-[var(--ssui-radius-sm)] py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-[color:var(--ssui-surface-muted)] data-[disabled]:pointer-events-none data-[disabled]:opacity-50", className), ...props, children: [_jsx("span", { className: "absolute left-2 flex size-3.5 items-center justify-center", children: _jsx(DropdownMenuPrimitive.ItemIndicator, { children: _jsx(Check, { className: "size-4" }) }) }), children] })));
23
+ export const DropdownMenuCheckboxItem = React.forwardRef(({ children, className, checked, ...props }, ref) => (_jsxs(DropdownMenuPrimitive.CheckboxItem, { ref: ref, checked: checked, className: cn("relative flex cursor-default select-none items-center rounded-[var(--ssui-radius-sm)] py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-[color:var(--ssui-overlay-hover)] data-[highlighted]:bg-[color:var(--ssui-overlay-hover)] data-[disabled]:pointer-events-none data-[disabled]:opacity-50", className), ...props, children: [_jsx("span", { className: "absolute left-2 flex size-3.5 items-center justify-center", children: _jsx(DropdownMenuPrimitive.ItemIndicator, { children: _jsx(Check, { className: "size-4" }) }) }), children] })));
21
24
  DropdownMenuCheckboxItem.displayName = DropdownMenuPrimitive.CheckboxItem.displayName;
22
- export const DropdownMenuRadioItem = React.forwardRef(({ children, className, ...props }, ref) => (_jsxs(DropdownMenuPrimitive.RadioItem, { ref: ref, className: cn("relative flex cursor-default select-none items-center rounded-[var(--ssui-radius-sm)] py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-[color:var(--ssui-surface-muted)] data-[disabled]:pointer-events-none data-[disabled]:opacity-50", className), ...props, children: [_jsx("span", { className: "absolute left-2 flex size-3.5 items-center justify-center", children: _jsx(DropdownMenuPrimitive.ItemIndicator, { children: _jsx(Circle, { className: "size-2 fill-current" }) }) }), children] })));
25
+ export const DropdownMenuRadioItem = React.forwardRef(({ children, className, ...props }, ref) => (_jsxs(DropdownMenuPrimitive.RadioItem, { ref: ref, className: cn("relative flex cursor-default select-none items-center rounded-[var(--ssui-radius-sm)] py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-[color:var(--ssui-overlay-hover)] data-[highlighted]:bg-[color:var(--ssui-overlay-hover)] data-[disabled]:pointer-events-none data-[disabled]:opacity-50", className), ...props, children: [_jsx("span", { className: "absolute left-2 flex size-3.5 items-center justify-center", children: _jsx(DropdownMenuPrimitive.ItemIndicator, { children: _jsx(Circle, { className: "size-2 fill-current" }) }) }), children] })));
23
26
  DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
24
27
  export const DropdownMenuLabel = React.forwardRef(({ className, inset, ...props }, ref) => (_jsx(DropdownMenuPrimitive.Label, { ref: ref, className: cn("px-2 py-1.5 text-xs font-medium text-[color:var(--ssui-text-muted)]", inset && "pl-8", className), ...props })));
25
28
  DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
@@ -15,6 +15,7 @@ export * from "./inline-rename-input.js";
15
15
  export * from "./input.js";
16
16
  export * from "./list-shell.js";
17
17
  export * from "./login.js";
18
+ export * from "./overflow-menu.js";
18
19
  export * from "./page-header.js";
19
20
  export * from "./request-access.js";
20
21
  export * from "./scroll-area.js";
@@ -15,6 +15,7 @@ export * from "./inline-rename-input.js";
15
15
  export * from "./input.js";
16
16
  export * from "./list-shell.js";
17
17
  export * from "./login.js";
18
+ export * from "./overflow-menu.js";
18
19
  export * from "./page-header.js";
19
20
  export * from "./request-access.js";
20
21
  export * from "./scroll-area.js";
@@ -0,0 +1,40 @@
1
+ import * as React from "react";
2
+ import { type IconButtonProps } from "./button.js";
3
+ export interface OverflowMenuProps {
4
+ /** Accessible label and tooltip for the trigger button. Defaults to "More actions". */
5
+ label?: string;
6
+ /** Which edge of the trigger the panel aligns to. Defaults to "end". */
7
+ align?: "start" | "center" | "end";
8
+ /** Trigger icon-button size. Defaults to "icon-sm". */
9
+ size?: IconButtonProps["size"];
10
+ /** Trigger icon-button visual variant. Defaults to "ghost". */
11
+ variant?: IconButtonProps["variant"];
12
+ /** Disable the trigger. */
13
+ disabled?: boolean;
14
+ /** Open the panel by default (uncontrolled). Mainly useful in tests. */
15
+ defaultOpen?: boolean;
16
+ /**
17
+ * Trap focus and lock scroll while open (Radix's default). Pass `false` for a
18
+ * lightweight menu that doesn't take the page modal — appropriate for inline
19
+ * affordances embedded in a larger surface.
20
+ */
21
+ modal?: boolean;
22
+ /**
23
+ * Portal the panel to `document.body` (the default). Pass `false` to render it
24
+ * inline next to the trigger, keeping it inside the trigger's DOM subtree.
25
+ */
26
+ portal?: boolean;
27
+ /** Extra classes for the trigger button. */
28
+ triggerClassName?: string;
29
+ /** Extra classes for the panel. */
30
+ contentClassName?: string;
31
+ /** Menu contents — typically `OverflowMenuItem`s separated by `OverflowMenuSeparator`. */
32
+ children: React.ReactNode;
33
+ }
34
+ /**
35
+ * A "⋯" icon-button that opens a dropdown of low-frequency, whole-object actions.
36
+ * Built on the Radix `DropdownMenu` primitives; compose the panel with
37
+ * `OverflowMenuItem` (and `OverflowMenuSeparator`) as children.
38
+ */
39
+ export declare function OverflowMenu({ label, align, size, variant, disabled, defaultOpen, modal, portal, triggerClassName, contentClassName, children, }: OverflowMenuProps): import("react/jsx-runtime").JSX.Element;
40
+ export { DropdownMenuItem as OverflowMenuItem, DropdownMenuSeparator as OverflowMenuSeparator, } from "./dropdown-menu.js";
@@ -0,0 +1,15 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { MoreHorizontal } from "lucide-react";
4
+ import * as React from "react";
5
+ import { IconButton } from "./button.js";
6
+ import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger, } from "./dropdown-menu.js";
7
+ /**
8
+ * A "⋯" icon-button that opens a dropdown of low-frequency, whole-object actions.
9
+ * Built on the Radix `DropdownMenu` primitives; compose the panel with
10
+ * `OverflowMenuItem` (and `OverflowMenuSeparator`) as children.
11
+ */
12
+ export function OverflowMenu({ label = "More actions", align = "end", size = "icon-sm", variant = "ghost", disabled, defaultOpen, modal, portal, triggerClassName, contentClassName, children, }) {
13
+ return (_jsxs(DropdownMenu, { defaultOpen: defaultOpen, modal: modal, children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsx(IconButton, { className: triggerClassName, disabled: disabled, label: label, size: size, variant: variant, children: _jsx(MoreHorizontal, {}) }) }), _jsx(DropdownMenuContent, { align: align, className: contentClassName, portal: portal, children: children })] }));
14
+ }
15
+ export { DropdownMenuItem as OverflowMenuItem, DropdownMenuSeparator as OverflowMenuSeparator, } from "./dropdown-menu.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forwardreach/saas-ui",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Brand-neutral React UI primitives and SaaS app patterns for ForwardReach-owned business applications.",
5
5
  "type": "module",
6
6
  "private": false,