Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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: (
<IconButton icon="menu" aria-label="Open menu" variant="text" size="sm" />
),
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),
},
};
20 changes: 17 additions & 3 deletions src/components/ui/navigation/dropdown-menu/DropdownMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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")}
/>
)}
Expand Down
22 changes: 14 additions & 8 deletions src/components/ui/surfaces/notch/Notch.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,22 @@ describe("Notch", () => {
<Notch width={200} height={150} notchWidth={40} notchHeight={50} notchSide="right" />,
);
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", () => {
const { container } = render(
<Notch width={200} height={150} notchWidth={40} notchHeight={50} notchSide="bottom" />,
);
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", () => {
Expand All @@ -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", () => {
Expand Down
15 changes: 11 additions & 4 deletions src/components/ui/surfaces/notch/Notch.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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.)
<svg
width={svgW + strokeWidth}
height={svgH + strokeWidth}
width={svgW}
height={svgH}
viewBox={`${-pad} ${-pad} ${svgW + strokeWidth} ${svgH + strokeWidth}`}
className={cn("pointer-events-none", className)}
style={style}
Expand All @@ -225,9 +229,12 @@ export function Notch({
const transform = getTransform(notchSide, pw, ph);

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 (rather
// than spilling the right & bottom strokes outside it).
<svg
width={svgW + strokeWidth}
height={svgH + strokeWidth}
width={svgW}
height={svgH}
viewBox={`${-pad} ${-pad} ${svgW + strokeWidth} ${svgH + strokeWidth}`}
className={cn("pointer-events-none", className)}
style={style}
Expand Down
Loading