@protolabsai/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/package.json +2 -1
- package/src/AppShell.stories.tsx +61 -0
- package/src/Menu.stories.tsx +76 -0
- package/src/index.tsx +197 -4
- package/src/styles.css +142 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@protolabsai/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public",
|
|
6
6
|
"registry": "https://registry.npmjs.org/"
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
"src"
|
|
20
20
|
],
|
|
21
21
|
"dependencies": {
|
|
22
|
+
"@radix-ui/react-dropdown-menu": "^2.1.17",
|
|
22
23
|
"@types/react": "^19.0.0",
|
|
23
24
|
"@types/react-dom": "^19.0.0",
|
|
24
25
|
"@protolabsai/design": "0.4.0"
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import { Badge, Button, PanelHeader, Tabs } from "./index";
|
|
4
|
+
|
|
5
|
+
const meta: Meta = { title: "Components/AppShell" };
|
|
6
|
+
export default meta;
|
|
7
|
+
type Story = StoryObj;
|
|
8
|
+
|
|
9
|
+
const Glyph = () => (
|
|
10
|
+
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5">
|
|
11
|
+
<rect x="2.5" y="2.5" width="11" height="11" rx="2" />
|
|
12
|
+
</svg>
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
export const Panel: Story = {
|
|
16
|
+
render: () => (
|
|
17
|
+
<div style={{ display: "grid", gap: 20, maxWidth: 560 }}>
|
|
18
|
+
<div style={{ border: "1px solid var(--pl-color-border)", borderRadius: 4 }}>
|
|
19
|
+
<PanelHeader
|
|
20
|
+
kicker="agent · streaming-router"
|
|
21
|
+
title="Activity"
|
|
22
|
+
actions={
|
|
23
|
+
<>
|
|
24
|
+
<Button>Filter</Button>
|
|
25
|
+
<Button variant="primary">New run</Button>
|
|
26
|
+
</>
|
|
27
|
+
}
|
|
28
|
+
/>
|
|
29
|
+
<div style={{ padding: 16, color: "var(--pl-color-fg-muted)", fontSize: 13 }}>panel body…</div>
|
|
30
|
+
</div>
|
|
31
|
+
|
|
32
|
+
<div style={{ border: "1px solid var(--pl-color-border)", borderRadius: 4 }}>
|
|
33
|
+
<PanelHeader compact title="Inbox" actions={<Badge status="warning">3 unread</Badge>} />
|
|
34
|
+
<div style={{ padding: 12, color: "var(--pl-color-fg-muted)", fontSize: 13 }}>compact, nested panel…</div>
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
),
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export const TabsWithSlots: Story = {
|
|
41
|
+
render: () => {
|
|
42
|
+
function Demo() {
|
|
43
|
+
const [active, setActive] = useState("activity");
|
|
44
|
+
return (
|
|
45
|
+
<Tabs
|
|
46
|
+
active={active}
|
|
47
|
+
onSelect={setActive}
|
|
48
|
+
items={[
|
|
49
|
+
{ id: "chat", label: "Chat", icon: <Glyph /> },
|
|
50
|
+
{ id: "activity", label: "Activity", icon: <Glyph />, badge: 12 },
|
|
51
|
+
{ id: "knowledge", label: "Knowledge", icon: <Glyph /> },
|
|
52
|
+
{ id: "plugins", label: "Plugins", icon: <Glyph />, badge: "new" },
|
|
53
|
+
{ id: "settings", label: "Settings", icon: <Glyph /> },
|
|
54
|
+
{ id: "billing", label: "Billing", icon: <Glyph />, locked: true, disabled: true },
|
|
55
|
+
]}
|
|
56
|
+
/>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
return <Demo />;
|
|
60
|
+
},
|
|
61
|
+
};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import { useRef } from "react";
|
|
3
|
+
import { Button, Menu, MenuHandle, MenuItem, MenuLabel, MenuSeparator, MenuSub } from "./index";
|
|
4
|
+
|
|
5
|
+
const meta: Meta = { title: "Components/Menu" };
|
|
6
|
+
export default meta;
|
|
7
|
+
type Story = StoryObj;
|
|
8
|
+
|
|
9
|
+
// Tiny inline icon so stories stay dependency-free.
|
|
10
|
+
const Dot = () => (
|
|
11
|
+
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5">
|
|
12
|
+
<circle cx="8" cy="8" r="3" />
|
|
13
|
+
</svg>
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
const Items = () => (
|
|
17
|
+
<>
|
|
18
|
+
<MenuLabel>Rail surface</MenuLabel>
|
|
19
|
+
<MenuItem icon={<Dot />}>Move up</MenuItem>
|
|
20
|
+
<MenuItem icon={<Dot />}>Move down</MenuItem>
|
|
21
|
+
<MenuSub label="Move to other rail" icon={<Dot />}>
|
|
22
|
+
<MenuItem>Left rail</MenuItem>
|
|
23
|
+
<MenuItem>Right rail</MenuItem>
|
|
24
|
+
</MenuSub>
|
|
25
|
+
<MenuSeparator />
|
|
26
|
+
<MenuItem disabled icon={<Dot />}>
|
|
27
|
+
Pin (coming soon)
|
|
28
|
+
</MenuItem>
|
|
29
|
+
<MenuItem destructive icon={<Dot />}>
|
|
30
|
+
Remove surface
|
|
31
|
+
</MenuItem>
|
|
32
|
+
</>
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
export const Trigger: Story = {
|
|
36
|
+
render: () => (
|
|
37
|
+
<Menu trigger={<Button>Surface actions ▾</Button>}>
|
|
38
|
+
<Items />
|
|
39
|
+
</Menu>
|
|
40
|
+
),
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export const RightClickAtCoords: Story = {
|
|
44
|
+
render: () => {
|
|
45
|
+
function Demo() {
|
|
46
|
+
const menu = useRef<MenuHandle>(null);
|
|
47
|
+
return (
|
|
48
|
+
<>
|
|
49
|
+
<div
|
|
50
|
+
onContextMenu={(e) => {
|
|
51
|
+
e.preventDefault();
|
|
52
|
+
menu.current?.open({ x: e.clientX, y: e.clientY });
|
|
53
|
+
}}
|
|
54
|
+
style={{
|
|
55
|
+
height: 180,
|
|
56
|
+
display: "grid",
|
|
57
|
+
placeItems: "center",
|
|
58
|
+
border: "1px dashed var(--pl-color-border-strong)",
|
|
59
|
+
borderRadius: 4,
|
|
60
|
+
color: "var(--pl-color-fg-muted)",
|
|
61
|
+
fontFamily: "var(--pl-font-mono)",
|
|
62
|
+
fontSize: 13,
|
|
63
|
+
userSelect: "none",
|
|
64
|
+
}}
|
|
65
|
+
>
|
|
66
|
+
right-click anywhere in this box
|
|
67
|
+
</div>
|
|
68
|
+
<Menu ref={menu}>
|
|
69
|
+
<Items />
|
|
70
|
+
</Menu>
|
|
71
|
+
</>
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
return <Demo />;
|
|
75
|
+
},
|
|
76
|
+
};
|
package/src/index.tsx
CHANGED
|
@@ -15,7 +15,18 @@ import type {
|
|
|
15
15
|
TextareaHTMLAttributes,
|
|
16
16
|
ThHTMLAttributes,
|
|
17
17
|
} from "react";
|
|
18
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
createContext,
|
|
20
|
+
forwardRef,
|
|
21
|
+
useCallback,
|
|
22
|
+
useContext,
|
|
23
|
+
useEffect,
|
|
24
|
+
useId,
|
|
25
|
+
useImperativeHandle,
|
|
26
|
+
useRef,
|
|
27
|
+
useState,
|
|
28
|
+
} from "react";
|
|
29
|
+
import * as RDropdown from "@radix-ui/react-dropdown-menu";
|
|
19
30
|
import "./styles.css";
|
|
20
31
|
|
|
21
32
|
const cx = (...parts: Array<string | false | undefined>) => parts.filter(Boolean).join(" ");
|
|
@@ -240,9 +251,19 @@ export function TextLink({
|
|
|
240
251
|
|
|
241
252
|
// ── App primitives (cockpit + future studio apps) ────────────────────────────
|
|
242
253
|
|
|
243
|
-
export type TabItem = {
|
|
254
|
+
export type TabItem = {
|
|
255
|
+
id: string;
|
|
256
|
+
label: ReactNode;
|
|
257
|
+
/** Leading icon (e.g. a Lucide glyph). */
|
|
258
|
+
icon?: ReactNode;
|
|
259
|
+
/** Trailing badge / count (e.g. unread inbox count). */
|
|
260
|
+
badge?: ReactNode;
|
|
261
|
+
disabled?: boolean;
|
|
262
|
+
locked?: boolean;
|
|
263
|
+
};
|
|
244
264
|
|
|
245
|
-
/** A horizontal tab strip with
|
|
265
|
+
/** A horizontal tab strip with optional icon/badge slots + disabled/locked
|
|
266
|
+
* support (gated workflows). */
|
|
246
267
|
export function Tabs({ items, active, onSelect }: { items: TabItem[]; active: string; onSelect: (id: string) => void }) {
|
|
247
268
|
return (
|
|
248
269
|
<div className="pl-tabs" role="tablist">
|
|
@@ -256,7 +277,13 @@ export function Tabs({ items, active, onSelect }: { items: TabItem[]; active: st
|
|
|
256
277
|
disabled={t.disabled}
|
|
257
278
|
onClick={() => onSelect(t.id)}
|
|
258
279
|
>
|
|
259
|
-
{t.
|
|
280
|
+
{t.icon != null && (
|
|
281
|
+
<span className="pl-tab__icon" aria-hidden>
|
|
282
|
+
{t.icon}
|
|
283
|
+
</span>
|
|
284
|
+
)}
|
|
285
|
+
<span className="pl-tab__label">{t.label}</span>
|
|
286
|
+
{t.badge != null && <span className="pl-tab__badge">{t.badge}</span>}
|
|
260
287
|
{t.locked ? (
|
|
261
288
|
<span className="pl-tab__lock" aria-hidden>
|
|
262
289
|
🔒
|
|
@@ -322,6 +349,33 @@ export function Field({
|
|
|
322
349
|
);
|
|
323
350
|
}
|
|
324
351
|
|
|
352
|
+
/** Dense operator-console panel header — title + optional kicker eyebrow +
|
|
353
|
+
* right-aligned actions slot. `compact` tightens it for nested/secondary
|
|
354
|
+
* panels. The most-used console composite. */
|
|
355
|
+
export function PanelHeader({
|
|
356
|
+
title,
|
|
357
|
+
kicker,
|
|
358
|
+
actions,
|
|
359
|
+
compact,
|
|
360
|
+
className,
|
|
361
|
+
}: {
|
|
362
|
+
title: ReactNode;
|
|
363
|
+
kicker?: ReactNode;
|
|
364
|
+
actions?: ReactNode;
|
|
365
|
+
compact?: boolean;
|
|
366
|
+
className?: string;
|
|
367
|
+
}) {
|
|
368
|
+
return (
|
|
369
|
+
<div className={cx("pl-panel-header", compact && "pl-panel-header--compact", className)}>
|
|
370
|
+
<div className="pl-panel-header__titles">
|
|
371
|
+
{kicker != null && <div className="pl-panel-header__kicker">{kicker}</div>}
|
|
372
|
+
<h2 className="pl-panel-header__title">{title}</h2>
|
|
373
|
+
</div>
|
|
374
|
+
{actions != null && <div className="pl-panel-header__actions">{actions}</div>}
|
|
375
|
+
</div>
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
|
|
325
379
|
// ── Overlays & feedback ──────────────────────────────────────────────────────
|
|
326
380
|
|
|
327
381
|
/** Esc-to-close + body scroll-lock while `open`. Shared by Dialog + Drawer. */
|
|
@@ -771,3 +825,142 @@ export function Checkbox({
|
|
|
771
825
|
</label>
|
|
772
826
|
);
|
|
773
827
|
}
|
|
828
|
+
|
|
829
|
+
// ── Menu / DropdownMenu (Radix-backed) ───────────────────────────────────────
|
|
830
|
+
// Radix owns keyboard nav, focus management, and collision-aware positioning —
|
|
831
|
+
// the reason this lives in the DS rather than being re-rolled per app. Styling
|
|
832
|
+
// is token-only over --pl-*. Supports a standard click trigger AND imperative
|
|
833
|
+
// open-at-coordinates for right-click / context menus.
|
|
834
|
+
|
|
835
|
+
export type MenuHandle = {
|
|
836
|
+
/** Open the menu. Pass viewport coords (e.g. from a contextmenu event) to
|
|
837
|
+
* open at a point; omit to open at the default anchor. */
|
|
838
|
+
open: (coords?: { x: number; y: number }) => void;
|
|
839
|
+
close: () => void;
|
|
840
|
+
};
|
|
841
|
+
|
|
842
|
+
export type MenuProps = {
|
|
843
|
+
/** Standard trigger (click to open). Omit and drive via the ref's
|
|
844
|
+
* open({x,y}) for right-click / imperative menus. */
|
|
845
|
+
trigger?: ReactNode;
|
|
846
|
+
children: ReactNode;
|
|
847
|
+
align?: "start" | "center" | "end";
|
|
848
|
+
/** Fires on open/close — e.g. to clear the app's context state on dismiss. */
|
|
849
|
+
onOpenChange?: (open: boolean) => void;
|
|
850
|
+
};
|
|
851
|
+
|
|
852
|
+
export const Menu = forwardRef<MenuHandle, MenuProps>(function Menu(
|
|
853
|
+
{ trigger, children, align = "start", onOpenChange },
|
|
854
|
+
ref,
|
|
855
|
+
) {
|
|
856
|
+
const [open, setOpen] = useState(false);
|
|
857
|
+
const [coords, setCoords] = useState<{ x: number; y: number } | null>(null);
|
|
858
|
+
const setOpenState = useCallback(
|
|
859
|
+
(o: boolean) => {
|
|
860
|
+
setOpen(o);
|
|
861
|
+
onOpenChange?.(o);
|
|
862
|
+
},
|
|
863
|
+
[onOpenChange],
|
|
864
|
+
);
|
|
865
|
+
useImperativeHandle(
|
|
866
|
+
ref,
|
|
867
|
+
() => ({
|
|
868
|
+
open: (c) => {
|
|
869
|
+
setCoords(c ?? null);
|
|
870
|
+
setOpenState(true);
|
|
871
|
+
},
|
|
872
|
+
close: () => setOpenState(false),
|
|
873
|
+
}),
|
|
874
|
+
[setOpenState],
|
|
875
|
+
);
|
|
876
|
+
return (
|
|
877
|
+
<RDropdown.Root open={open} onOpenChange={setOpenState} modal={false}>
|
|
878
|
+
{trigger != null ? (
|
|
879
|
+
<RDropdown.Trigger asChild>{trigger}</RDropdown.Trigger>
|
|
880
|
+
) : (
|
|
881
|
+
<RDropdown.Trigger asChild>
|
|
882
|
+
<span
|
|
883
|
+
aria-hidden
|
|
884
|
+
className="pl-menu__anchor"
|
|
885
|
+
style={coords ? { position: "fixed", left: coords.x, top: coords.y } : undefined}
|
|
886
|
+
/>
|
|
887
|
+
</RDropdown.Trigger>
|
|
888
|
+
)}
|
|
889
|
+
<RDropdown.Portal>
|
|
890
|
+
<RDropdown.Content className="pl-menu" align={align} sideOffset={4} collisionPadding={8} loop>
|
|
891
|
+
{children}
|
|
892
|
+
</RDropdown.Content>
|
|
893
|
+
</RDropdown.Portal>
|
|
894
|
+
</RDropdown.Root>
|
|
895
|
+
);
|
|
896
|
+
});
|
|
897
|
+
|
|
898
|
+
export function MenuItem({
|
|
899
|
+
icon,
|
|
900
|
+
disabled,
|
|
901
|
+
destructive,
|
|
902
|
+
onSelect,
|
|
903
|
+
children,
|
|
904
|
+
}: {
|
|
905
|
+
icon?: ReactNode;
|
|
906
|
+
disabled?: boolean;
|
|
907
|
+
/** Error-toned (delete, remove, etc.). */
|
|
908
|
+
destructive?: boolean;
|
|
909
|
+
onSelect?: () => void;
|
|
910
|
+
children: ReactNode;
|
|
911
|
+
}) {
|
|
912
|
+
return (
|
|
913
|
+
<RDropdown.Item
|
|
914
|
+
className={cx("pl-menu__item", destructive && "pl-menu__item--destructive")}
|
|
915
|
+
disabled={disabled}
|
|
916
|
+
onSelect={onSelect}
|
|
917
|
+
>
|
|
918
|
+
{icon != null && (
|
|
919
|
+
<span className="pl-menu__icon" aria-hidden>
|
|
920
|
+
{icon}
|
|
921
|
+
</span>
|
|
922
|
+
)}
|
|
923
|
+
<span className="pl-menu__label">{children}</span>
|
|
924
|
+
</RDropdown.Item>
|
|
925
|
+
);
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
export function MenuSeparator() {
|
|
929
|
+
return <RDropdown.Separator className="pl-menu__sep" />;
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
export function MenuLabel({ children }: { children: ReactNode }) {
|
|
933
|
+
return <RDropdown.Label className="pl-menu__group-label">{children}</RDropdown.Label>;
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
/** Nested submenu — put MenuItem/MenuSeparator children inside. */
|
|
937
|
+
export function MenuSub({
|
|
938
|
+
label,
|
|
939
|
+
icon,
|
|
940
|
+
children,
|
|
941
|
+
}: {
|
|
942
|
+
label: ReactNode;
|
|
943
|
+
icon?: ReactNode;
|
|
944
|
+
children: ReactNode;
|
|
945
|
+
}) {
|
|
946
|
+
return (
|
|
947
|
+
<RDropdown.Sub>
|
|
948
|
+
<RDropdown.SubTrigger className="pl-menu__item pl-menu__subtrigger">
|
|
949
|
+
{icon != null && (
|
|
950
|
+
<span className="pl-menu__icon" aria-hidden>
|
|
951
|
+
{icon}
|
|
952
|
+
</span>
|
|
953
|
+
)}
|
|
954
|
+
<span className="pl-menu__label">{label}</span>
|
|
955
|
+
<span className="pl-menu__subarrow" aria-hidden>
|
|
956
|
+
›
|
|
957
|
+
</span>
|
|
958
|
+
</RDropdown.SubTrigger>
|
|
959
|
+
<RDropdown.Portal>
|
|
960
|
+
<RDropdown.SubContent className="pl-menu" sideOffset={2} alignOffset={-4} collisionPadding={8}>
|
|
961
|
+
{children}
|
|
962
|
+
</RDropdown.SubContent>
|
|
963
|
+
</RDropdown.Portal>
|
|
964
|
+
</RDropdown.Sub>
|
|
965
|
+
);
|
|
966
|
+
}
|
package/src/styles.css
CHANGED
|
@@ -472,6 +472,9 @@ a.pl-row:hover {
|
|
|
472
472
|
border-bottom: var(--pl-border-width) solid var(--pl-color-border);
|
|
473
473
|
}
|
|
474
474
|
.pl-tab {
|
|
475
|
+
display: inline-flex;
|
|
476
|
+
align-items: center;
|
|
477
|
+
gap: 6px;
|
|
475
478
|
background: none;
|
|
476
479
|
border: none;
|
|
477
480
|
border-bottom: 2px solid transparent;
|
|
@@ -482,6 +485,32 @@ a.pl-row:hover {
|
|
|
482
485
|
cursor: pointer;
|
|
483
486
|
border-radius: var(--pl-radius) var(--pl-radius) 0 0;
|
|
484
487
|
}
|
|
488
|
+
.pl-tab__icon {
|
|
489
|
+
display: inline-flex;
|
|
490
|
+
align-items: center;
|
|
491
|
+
}
|
|
492
|
+
.pl-tab__icon svg {
|
|
493
|
+
width: 15px;
|
|
494
|
+
height: 15px;
|
|
495
|
+
}
|
|
496
|
+
.pl-tab__badge {
|
|
497
|
+
display: inline-flex;
|
|
498
|
+
align-items: center;
|
|
499
|
+
justify-content: center;
|
|
500
|
+
min-width: 16px;
|
|
501
|
+
height: 16px;
|
|
502
|
+
padding: 0 5px;
|
|
503
|
+
font-family: var(--pl-font-mono);
|
|
504
|
+
font-size: 10px;
|
|
505
|
+
line-height: 1;
|
|
506
|
+
color: var(--pl-color-fg-muted);
|
|
507
|
+
background: var(--pl-color-bg-subtle);
|
|
508
|
+
border: var(--pl-border-width) solid var(--pl-color-border);
|
|
509
|
+
border-radius: 999px;
|
|
510
|
+
}
|
|
511
|
+
.pl-tab--active .pl-tab__badge {
|
|
512
|
+
color: var(--pl-color-fg);
|
|
513
|
+
}
|
|
485
514
|
.pl-tab:hover:not(:disabled) {
|
|
486
515
|
color: var(--pl-color-fg);
|
|
487
516
|
}
|
|
@@ -579,6 +608,43 @@ a.pl-row:hover {
|
|
|
579
608
|
border-color: var(--pl-color-fg);
|
|
580
609
|
}
|
|
581
610
|
|
|
611
|
+
/* ── PanelHeader (console panel header) ──────────────────────────────────────── */
|
|
612
|
+
.pl-panel-header {
|
|
613
|
+
display: flex;
|
|
614
|
+
align-items: center;
|
|
615
|
+
justify-content: space-between;
|
|
616
|
+
gap: var(--pl-space-3);
|
|
617
|
+
padding: var(--pl-space-3) var(--pl-space-4);
|
|
618
|
+
border-bottom: var(--pl-border-width) solid var(--pl-color-border);
|
|
619
|
+
}
|
|
620
|
+
.pl-panel-header__kicker {
|
|
621
|
+
font-family: var(--pl-font-mono);
|
|
622
|
+
font-size: 11px;
|
|
623
|
+
text-transform: uppercase;
|
|
624
|
+
letter-spacing: 0.06em;
|
|
625
|
+
color: var(--pl-color-fg-muted);
|
|
626
|
+
margin-bottom: 2px;
|
|
627
|
+
}
|
|
628
|
+
.pl-panel-header__title {
|
|
629
|
+
margin: 0;
|
|
630
|
+
font-size: 15px;
|
|
631
|
+
font-weight: var(--pl-font-weight-medium);
|
|
632
|
+
line-height: 1.2;
|
|
633
|
+
color: var(--pl-color-fg);
|
|
634
|
+
}
|
|
635
|
+
.pl-panel-header__actions {
|
|
636
|
+
display: flex;
|
|
637
|
+
align-items: center;
|
|
638
|
+
gap: var(--pl-space-2);
|
|
639
|
+
flex-shrink: 0;
|
|
640
|
+
}
|
|
641
|
+
.pl-panel-header--compact {
|
|
642
|
+
padding: var(--pl-space-2) var(--pl-space-3);
|
|
643
|
+
}
|
|
644
|
+
.pl-panel-header--compact .pl-panel-header__title {
|
|
645
|
+
font-size: 13px;
|
|
646
|
+
}
|
|
647
|
+
|
|
582
648
|
/* ── Overlay scrim (Dialog + Drawer) ─────────────────────────────────────────── */
|
|
583
649
|
.pl-overlay {
|
|
584
650
|
position: fixed;
|
|
@@ -1097,6 +1163,82 @@ a.pl-row:hover {
|
|
|
1097
1163
|
color: var(--pl-color-fg);
|
|
1098
1164
|
}
|
|
1099
1165
|
|
|
1166
|
+
/* ── Menu / DropdownMenu (Radix-backed) ──────────────────────────────────────── */
|
|
1167
|
+
.pl-menu__anchor {
|
|
1168
|
+
width: 0;
|
|
1169
|
+
height: 0;
|
|
1170
|
+
pointer-events: none;
|
|
1171
|
+
}
|
|
1172
|
+
.pl-menu {
|
|
1173
|
+
min-width: 180px;
|
|
1174
|
+
padding: 4px;
|
|
1175
|
+
background: var(--pl-color-bg-raised);
|
|
1176
|
+
border: var(--pl-border-width) solid var(--pl-color-border-strong);
|
|
1177
|
+
border-radius: var(--pl-radius);
|
|
1178
|
+
box-shadow: var(--pl-shadow-popover);
|
|
1179
|
+
z-index: 1200;
|
|
1180
|
+
}
|
|
1181
|
+
.pl-menu__item {
|
|
1182
|
+
display: flex;
|
|
1183
|
+
align-items: center;
|
|
1184
|
+
gap: 8px;
|
|
1185
|
+
padding: 6px 8px;
|
|
1186
|
+
font-size: 13px;
|
|
1187
|
+
color: var(--pl-color-fg);
|
|
1188
|
+
border-radius: calc(var(--pl-radius) - 1px);
|
|
1189
|
+
cursor: pointer;
|
|
1190
|
+
outline: none;
|
|
1191
|
+
user-select: none;
|
|
1192
|
+
}
|
|
1193
|
+
/* Radix sets data-highlighted on the keyboard/pointer-focused item. */
|
|
1194
|
+
.pl-menu__item[data-highlighted] {
|
|
1195
|
+
background: var(--pl-color-bg-hover);
|
|
1196
|
+
}
|
|
1197
|
+
.pl-menu__item[data-disabled] {
|
|
1198
|
+
color: var(--pl-color-fg-muted);
|
|
1199
|
+
opacity: 0.5;
|
|
1200
|
+
pointer-events: none;
|
|
1201
|
+
}
|
|
1202
|
+
.pl-menu__item--destructive {
|
|
1203
|
+
color: var(--pl-color-status-error);
|
|
1204
|
+
}
|
|
1205
|
+
.pl-menu__item--destructive[data-highlighted] {
|
|
1206
|
+
background: color-mix(in oklch, var(--pl-color-status-error) 16%, transparent);
|
|
1207
|
+
}
|
|
1208
|
+
.pl-menu__icon {
|
|
1209
|
+
display: inline-flex;
|
|
1210
|
+
align-items: center;
|
|
1211
|
+
color: var(--pl-color-fg-muted);
|
|
1212
|
+
}
|
|
1213
|
+
.pl-menu__icon svg {
|
|
1214
|
+
width: 15px;
|
|
1215
|
+
height: 15px;
|
|
1216
|
+
}
|
|
1217
|
+
.pl-menu__item--destructive .pl-menu__icon {
|
|
1218
|
+
color: var(--pl-color-status-error);
|
|
1219
|
+
}
|
|
1220
|
+
.pl-menu__label {
|
|
1221
|
+
flex: 1;
|
|
1222
|
+
}
|
|
1223
|
+
.pl-menu__subarrow {
|
|
1224
|
+
color: var(--pl-color-fg-muted);
|
|
1225
|
+
font-size: 14px;
|
|
1226
|
+
line-height: 1;
|
|
1227
|
+
}
|
|
1228
|
+
.pl-menu__sep {
|
|
1229
|
+
height: 1px;
|
|
1230
|
+
margin: 4px 0;
|
|
1231
|
+
background: var(--pl-color-border);
|
|
1232
|
+
}
|
|
1233
|
+
.pl-menu__group-label {
|
|
1234
|
+
padding: 4px 8px;
|
|
1235
|
+
font-family: var(--pl-font-mono);
|
|
1236
|
+
font-size: 10px;
|
|
1237
|
+
text-transform: uppercase;
|
|
1238
|
+
letter-spacing: 0.06em;
|
|
1239
|
+
color: var(--pl-color-fg-muted);
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1100
1242
|
/* Respect reduced-motion for every animation this package introduces. */
|
|
1101
1243
|
@media (prefers-reduced-motion: reduce) {
|
|
1102
1244
|
.pl-drawer,
|