diff --git a/package.json b/package.json
index 344230f7d..9255dd2be 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "@mirrorstack-ai/web-ui-kit",
"packageManager": "pnpm@10.29.3",
- "version": "0.5.10",
+ "version": "0.5.11",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
diff --git a/src/components/ui/navigation/dropdown-menu/DropdownMenu.stories.tsx b/src/components/ui/navigation/dropdown-menu/DropdownMenu.stories.tsx
index 859a66b98..51429069d 100644
--- a/src/components/ui/navigation/dropdown-menu/DropdownMenu.stories.tsx
+++ b/src/components/ui/navigation/dropdown-menu/DropdownMenu.stories.tsx
@@ -31,3 +31,27 @@ export const Playground: Story = {
onSelect: (item) => console.log("Selected:", item.id),
},
};
+
+/**
+ * The notch head tuned to a small trigger: `notchWidth`/`notchHeight` size the
+ * tab to the icon button, and `notchRadius`/`notchInverseRadius` round the head
+ * down to the button's own corner radius instead of the default bulbous 16.
+ */
+export const TightHead: Story = {
+ args: {
+ trigger: (
+
+ ),
+ notchWidth: 34,
+ notchHeight: 38,
+ notchRadius: 8,
+ notchInverseRadius: 6,
+ items: [
+ { id: "how", label: "How it works", icon: "play_circle" },
+ { id: "source", label: "View source", icon: "code" },
+ { type: "separator" as const },
+ { id: "subscribe", label: "Get early access", icon: "mail" },
+ ],
+ onSelect: (item) => console.log("Selected:", item.id),
+ },
+};
diff --git a/src/components/ui/navigation/dropdown-menu/DropdownMenu.tsx b/src/components/ui/navigation/dropdown-menu/DropdownMenu.tsx
index 6446361a4..e6966add4 100644
--- a/src/components/ui/navigation/dropdown-menu/DropdownMenu.tsx
+++ b/src/components/ui/navigation/dropdown-menu/DropdownMenu.tsx
@@ -18,6 +18,7 @@ const DD_NOTCH_W = 52;
const DD_NOTCH_H = 46;
const DD_R = 16;
const DD_IR = 10;
+const DD_SW = 1.5;
// Item density tokens per `size` — "lg" pads up to comfortable touch targets.
const SIZES = {
@@ -56,6 +57,16 @@ export interface DropdownMenuProps {
notchWidth?: number;
/** Height of the notch tab (SVG units). Default `46`. */
notchHeight?: number;
+ /** Corner radius of the notch outline (SVG units). Default `16`. Lower it to
+ * match a small trigger's own radius for a less-rounded head (e.g. an icon
+ * button with an 8px radius). */
+ notchRadius?: number;
+ /** Radius of the notch's inverse (concave) corners, where the tab curves back
+ * into the card (SVG units). Default `10`. Usually scaled down alongside
+ * `notchRadius` for a tighter head. */
+ notchInverseRadius?: number;
+ /** Stroke (border) width of the notch outline, in px. Default `1.5`. */
+ notchStrokeWidth?: number;
/** Render the kit's signature notch wrapping the trigger. Default `true`. Set
* to `false` for a plain floating menu (e.g. selects, period pickers). */
useNotch?: boolean;
@@ -89,6 +100,9 @@ export function DropdownMenu({
offset = 0,
notchWidth = DD_NOTCH_W,
notchHeight = DD_NOTCH_H,
+ notchRadius = DD_R,
+ notchInverseRadius = DD_IR,
+ notchStrokeWidth = DD_SW,
useNotch = true,
placement = "bottom",
size = "md",
@@ -207,10 +221,10 @@ export function DropdownMenu({
notchHeight={notchHeight}
notchSide={openUp ? "top" : "bottom"}
notchOffset={offset}
- radius={DD_R}
- inverseRadius={DD_IR}
+ radius={notchRadius}
+ inverseRadius={notchInverseRadius}
stroke="var(--color-primary)"
- strokeWidth={1.5}
+ strokeWidth={notchStrokeWidth}
className={cn("absolute left-0", openUp ? "bottom-0" : "top-0")}
/>
)}
diff --git a/src/components/ui/surfaces/notch/Notch.test.tsx b/src/components/ui/surfaces/notch/Notch.test.tsx
index dc3b387ad..44c6bd566 100644
--- a/src/components/ui/surfaces/notch/Notch.test.tsx
+++ b/src/components/ui/surfaces/notch/Notch.test.tsx
@@ -19,8 +19,12 @@ describe("Notch", () => {
,
);
const svg = container.querySelector("svg");
- expect(svg).toHaveAttribute("width", "241");
- expect(svg).toHaveAttribute("height", "151");
+ // Element is the content box (width+notchWidth × height); the centered
+ // stroke is accommodated by the padded viewBox (content + strokeWidth,
+ // offset by -strokeWidth/2) so the border stays inside the box.
+ expect(svg).toHaveAttribute("width", "240");
+ expect(svg).toHaveAttribute("height", "150");
+ expect(svg).toHaveAttribute("viewBox", "-0.5 -0.5 241 151");
});
it("sets correct SVG dimensions for bottom notch", () => {
@@ -28,8 +32,9 @@ describe("Notch", () => {
,
);
const svg = container.querySelector("svg");
- expect(svg).toHaveAttribute("width", "201");
- expect(svg).toHaveAttribute("height", "201");
+ expect(svg).toHaveAttribute("width", "200");
+ expect(svg).toHaveAttribute("height", "200");
+ expect(svg).toHaveAttribute("viewBox", "-0.5 -0.5 201 201");
});
it("renders head only", () => {
@@ -38,10 +43,11 @@ describe("Notch", () => {
);
const svg = container.querySelector("svg");
// offset=0 → atStart=true, topIr=0, botIr=6
- // pathW = 60 + 6 = 66, + stroke = 67
- // pathH = 40 + 0 + 6 = 46, + stroke = 47
- expect(svg).toHaveAttribute("width", "67");
- expect(svg).toHaveAttribute("height", "47");
+ // pathW = 60 + 6 = 66 (element width); pathH = 40 + 0 + 6 = 46 (element height)
+ // viewBox pads by strokeWidth so the stroke stays inside the box: 67 × 47.
+ expect(svg).toHaveAttribute("width", "66");
+ expect(svg).toHaveAttribute("height", "46");
+ expect(svg).toHaveAttribute("viewBox", "-0.5 -0.5 67 47");
});
it("applies fill and stroke", () => {
diff --git a/src/components/ui/surfaces/notch/Notch.tsx b/src/components/ui/surfaces/notch/Notch.tsx
index edfc4a537..518407222 100644
--- a/src/components/ui/surfaces/notch/Notch.tsx
+++ b/src/components/ui/surfaces/notch/Notch.tsx
@@ -197,9 +197,13 @@ export function Notch({
const transform = getTransform(notchSide, pathW, pathH);
return (
+ // Element sized to the CONTENT box; the viewBox stays padded by `pad` on
+ // every side so the centered stroke renders fully INSIDE the box. (Sizing
+ // the element to svgW+strokeWidth instead pins it `pad` larger and pushes
+ // the right & bottom strokes outside the box.)