@protolabsai/ui 0.9.0 → 0.10.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@protolabsai/ui",
3
- "version": "0.9.0",
3
+ "version": "0.10.0",
4
4
  "publishConfig": {
5
5
  "access": "public",
6
6
  "registry": "https://registry.npmjs.org/"
@@ -31,6 +31,7 @@
31
31
  "@dnd-kit/sortable": "^10.0.0",
32
32
  "@dnd-kit/utilities": "^3.2.2",
33
33
  "@radix-ui/react-dropdown-menu": "^2.1.17",
34
+ "@radix-ui/react-popover": "^1.1.16",
34
35
  "@types/react": "^19.0.0",
35
36
  "@types/react-dom": "^19.0.0",
36
37
  "@protolabsai/design": "0.5.0"
@@ -2,12 +2,41 @@ import type { Meta, StoryObj } from "@storybook/react";
2
2
  import { useState } from "react";
3
3
  import { Button } from "./primitives";
4
4
  import { Field } from "./forms";
5
- import { ConfirmDialog, Dialog, Drawer, Tooltip, ToastProvider, useToast } from "./overlays";
5
+ import { ConfirmDialog, Dialog, Drawer, Popover, PopoverClose, Tooltip, ToastProvider, useToast } from "./overlays";
6
6
 
7
7
  const meta: Meta = { title: "Components/Overlays" };
8
8
  export default meta;
9
9
  type Story = StoryObj;
10
10
 
11
+ export const PopoverStory: Story = {
12
+ name: "Popover",
13
+ render: () => (
14
+ <div style={{ display: "flex", gap: 40, padding: 40, justifyContent: "center" }}>
15
+ <Popover trigger={<Button>Filter ▾</Button>}>
16
+ <div style={{ display: "grid", gap: 10 }}>
17
+ <Field label="Search" value="" placeholder="agent name…" />
18
+ <div style={{ display: "flex", justifyContent: "flex-end", gap: 8 }}>
19
+ <PopoverClose>
20
+ <Button size="sm" variant="ghost">
21
+ Cancel
22
+ </Button>
23
+ </PopoverClose>
24
+ <PopoverClose>
25
+ <Button size="sm" variant="primary">
26
+ Apply
27
+ </Button>
28
+ </PopoverClose>
29
+ </div>
30
+ </div>
31
+ </Popover>
32
+ <Popover side="right" align="start" trigger={<Button variant="ghost">Details →</Button>}>
33
+ Anchored floating content — collision-aware, dismiss on outside-click or Esc. The generic
34
+ primitive under filters, pickers, and comboboxes.
35
+ </Popover>
36
+ </div>
37
+ ),
38
+ };
39
+
11
40
  export const Modal: Story = {
12
41
  render: () => {
13
42
  function Demo() {
package/src/overlays.tsx CHANGED
@@ -1,5 +1,6 @@
1
1
  import type { ReactNode, RefObject } from "react";
2
2
  import { createContext, useCallback, useContext, useEffect, useId, useRef, useState } from "react";
3
+ import * as RPopover from "@radix-ui/react-popover";
3
4
  import { cx } from "./internal";
4
5
  import type { Status } from "./internal";
5
6
  import { Button } from "./primitives";
@@ -297,3 +298,47 @@ export function Tooltip({
297
298
  </span>
298
299
  );
299
300
  }
301
+
302
+ /** Anchored floating content (Radix-backed) — the generic primitive under
303
+ * filters, pickers, comboboxes, etc. Collision-aware positioning + dismissal
304
+ * handled by Radix. Controlled or uncontrolled via `open` / `onOpenChange`. */
305
+ export function Popover({
306
+ trigger,
307
+ children,
308
+ open,
309
+ onOpenChange,
310
+ side = "bottom",
311
+ align = "center",
312
+ className,
313
+ }: {
314
+ /** The element that opens the popover (rendered as the anchor). */
315
+ trigger: ReactNode;
316
+ children: ReactNode;
317
+ open?: boolean;
318
+ onOpenChange?: (open: boolean) => void;
319
+ side?: "top" | "right" | "bottom" | "left";
320
+ align?: "start" | "center" | "end";
321
+ className?: string;
322
+ }) {
323
+ return (
324
+ <RPopover.Root open={open} onOpenChange={onOpenChange}>
325
+ <RPopover.Trigger asChild>{trigger}</RPopover.Trigger>
326
+ <RPopover.Portal>
327
+ <RPopover.Content
328
+ className={cx("pl-popover", className)}
329
+ side={side}
330
+ align={align}
331
+ sideOffset={6}
332
+ collisionPadding={8}
333
+ >
334
+ {children}
335
+ </RPopover.Content>
336
+ </RPopover.Portal>
337
+ </RPopover.Root>
338
+ );
339
+ }
340
+
341
+ /** Dismiss the enclosing Popover — wrap a Button/element (e.g. a "Done" action). */
342
+ export function PopoverClose({ children }: { children: ReactNode }) {
343
+ return <RPopover.Close asChild>{children}</RPopover.Close>;
344
+ }
@@ -293,3 +293,18 @@
293
293
  top: 50%;
294
294
  transform: translateY(-50%);
295
295
  }
296
+
297
+ /* ── Popover (Radix-backed) ──────────────────────────────────────────────────── */
298
+ .pl-popover {
299
+ min-width: 200px;
300
+ max-width: 320px;
301
+ padding: var(--pl-space-3);
302
+ font-size: 13px;
303
+ line-height: 1.5;
304
+ color: var(--pl-color-fg);
305
+ background: var(--pl-color-bg-raised);
306
+ border: var(--pl-border-width) solid var(--pl-color-border-strong);
307
+ border-radius: var(--pl-radius);
308
+ box-shadow: var(--pl-shadow-popover);
309
+ z-index: 1200;
310
+ }