@protolabsai/ui 0.9.0 → 0.11.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.full.stories.tsx +16 -2
- package/src/Overlays.stories.tsx +30 -1
- package/src/Primitives.stories.tsx +37 -1
- package/src/app-shell.tsx +57 -29
- package/src/forms.tsx +14 -16
- package/src/overlays.tsx +45 -0
- package/src/primitives.tsx +66 -0
- package/src/styles/app-shell.css +37 -1
- package/src/styles/overlays.css +15 -0
- package/src/styles/primitives.css +68 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@protolabsai/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.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"
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
2
|
import { useState } from "react";
|
|
3
|
-
import { Button } from "./primitives";
|
|
3
|
+
import { Badge, Button } from "./primitives";
|
|
4
4
|
import { PanelHeader } from "./navigation";
|
|
5
|
-
import {
|
|
5
|
+
import { StatusDot } from "./data";
|
|
6
|
+
import { AppShell, MobileNav, SurfaceRail, UtilityBar } from "./app-shell";
|
|
6
7
|
import type { MobileItem, RailItem } from "./app-shell";
|
|
7
8
|
|
|
8
9
|
const meta: Meta = { title: "Components/AppShell" };
|
|
@@ -83,6 +84,19 @@ export const Full: Story = {
|
|
|
83
84
|
{surfaceBody(activeRight)}
|
|
84
85
|
</>
|
|
85
86
|
}
|
|
87
|
+
utilityBar={
|
|
88
|
+
<UtilityBar
|
|
89
|
+
start={<StatusDot status="success" pulse label="connected · 3 agents" />}
|
|
90
|
+
end={
|
|
91
|
+
<>
|
|
92
|
+
<Button size="sm" variant="ghost">
|
|
93
|
+
Docs
|
|
94
|
+
</Button>
|
|
95
|
+
<Badge status="info">v0.11.0</Badge>
|
|
96
|
+
</>
|
|
97
|
+
}
|
|
98
|
+
/>
|
|
99
|
+
}
|
|
86
100
|
mobileItems={mobile}
|
|
87
101
|
mobileActiveId={activeLeft}
|
|
88
102
|
onMobileSelect={setActiveLeft}
|
package/src/Overlays.stories.tsx
CHANGED
|
@@ -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() {
|
|
@@ -1,10 +1,46 @@
|
|
|
1
1
|
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
-
import {
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import { Avatar, AvatarGroup, Callout, Divider, Kbd, Tag, TextLink } from "./primitives";
|
|
3
4
|
|
|
4
5
|
const meta: Meta = { title: "Components/Primitives/Atoms" };
|
|
5
6
|
export default meta;
|
|
6
7
|
type Story = StoryObj;
|
|
7
8
|
|
|
9
|
+
export const Avatars: Story = {
|
|
10
|
+
render: () => (
|
|
11
|
+
<div style={{ display: "flex", gap: 24, alignItems: "center" }}>
|
|
12
|
+
<Avatar name="Jon Reed" />
|
|
13
|
+
<Avatar name="Cindi Vox" size={40} />
|
|
14
|
+
<Avatar name="ORBIS" size={22} />
|
|
15
|
+
<AvatarGroup>
|
|
16
|
+
<Avatar name="Jon Reed" />
|
|
17
|
+
<Avatar name="Cindi Vox" />
|
|
18
|
+
<Avatar name="Beads" />
|
|
19
|
+
<Avatar name="+3" />
|
|
20
|
+
</AvatarGroup>
|
|
21
|
+
</div>
|
|
22
|
+
),
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const Tags: Story = {
|
|
26
|
+
render: () => {
|
|
27
|
+
function Demo() {
|
|
28
|
+
const [tags, setTags] = useState(["streaming", "a2a", "voice", "router"]);
|
|
29
|
+
return (
|
|
30
|
+
<div style={{ display: "flex", gap: 8, flexWrap: "wrap", alignItems: "center" }}>
|
|
31
|
+
<Tag>read-only</Tag>
|
|
32
|
+
{tags.map((t) => (
|
|
33
|
+
<Tag key={t} onRemove={() => setTags((ts) => ts.filter((x) => x !== t))}>
|
|
34
|
+
{t}
|
|
35
|
+
</Tag>
|
|
36
|
+
))}
|
|
37
|
+
</div>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
return <Demo />;
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
|
|
8
44
|
export const Callouts: Story = {
|
|
9
45
|
render: () => (
|
|
10
46
|
<div style={{ display: "grid", gap: 16, maxWidth: 560 }}>
|
package/src/app-shell.tsx
CHANGED
|
@@ -280,6 +280,9 @@ export type AppShellProps = {
|
|
|
280
280
|
onMobileSelect?: (id: string) => void;
|
|
281
281
|
quickBarIds?: string[];
|
|
282
282
|
mobileBreakpoint?: number;
|
|
283
|
+
/** Bottom 40px track — global utility actions / status / tickers. Presentation
|
|
284
|
+
* only; the host fills it (compose a `UtilityBar`). Desktop only. */
|
|
285
|
+
utilityBar?: ReactNode;
|
|
283
286
|
className?: string;
|
|
284
287
|
};
|
|
285
288
|
|
|
@@ -308,6 +311,7 @@ export function AppShell({
|
|
|
308
311
|
onMobileSelect,
|
|
309
312
|
quickBarIds,
|
|
310
313
|
mobileBreakpoint = 768,
|
|
314
|
+
utilityBar,
|
|
311
315
|
className,
|
|
312
316
|
}: AppShellProps) {
|
|
313
317
|
const isMobile = useIsMobile(mobileBreakpoint);
|
|
@@ -445,35 +449,38 @@ export function AppShell({
|
|
|
445
449
|
const ctxRight = onRailContextMenu ? (e: ReactMouseEvent, id: string) => onRailContextMenu("right", e, id) : undefined;
|
|
446
450
|
|
|
447
451
|
const renderShell = (leftRail: ReactNode, rightRail: ReactNode) => (
|
|
448
|
-
<div className={cx("pl-appshell", className)}>
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
452
|
+
<div className={cx("pl-appshell-frame", className)}>
|
|
453
|
+
<div className="pl-appshell">
|
|
454
|
+
{leftRail}
|
|
455
|
+
<main className="pl-appshell__col pl-appshell__col--left">{leftContent}</main>
|
|
456
|
+
{showRight && (
|
|
457
|
+
<div
|
|
458
|
+
className="pl-appshell__handle"
|
|
459
|
+
role="separator"
|
|
460
|
+
aria-orientation="vertical"
|
|
461
|
+
aria-label="Resize right panel"
|
|
462
|
+
aria-valuenow={rightWidth}
|
|
463
|
+
aria-valuemin={minRightWidth}
|
|
464
|
+
aria-valuemax={maxRightWidth}
|
|
465
|
+
tabIndex={0}
|
|
466
|
+
onPointerDown={onPointerDown}
|
|
467
|
+
onPointerMove={onPointerMove}
|
|
468
|
+
onPointerUp={onPointerUp}
|
|
469
|
+
onKeyDown={onKeyDown}
|
|
470
|
+
onDoubleClick={() => onCollapse?.(true)}
|
|
471
|
+
/>
|
|
472
|
+
)}
|
|
473
|
+
{showRight && (
|
|
474
|
+
<aside
|
|
475
|
+
className="pl-appshell__col pl-appshell__col--right"
|
|
476
|
+
style={{ flex: `0 0 ${rightWidth}px`, width: rightWidth }}
|
|
477
|
+
>
|
|
478
|
+
{rightContent}
|
|
479
|
+
</aside>
|
|
480
|
+
)}
|
|
481
|
+
{rightRail}
|
|
482
|
+
</div>
|
|
483
|
+
{utilityBar != null && <div className="pl-appshell__utility">{utilityBar}</div>}
|
|
477
484
|
</div>
|
|
478
485
|
);
|
|
479
486
|
|
|
@@ -511,3 +518,24 @@ export function AppShell({
|
|
|
511
518
|
</DndContext>
|
|
512
519
|
);
|
|
513
520
|
}
|
|
521
|
+
|
|
522
|
+
/** Bottom utility-bar chrome — start cluster · spacer · end cluster. Compose
|
|
523
|
+
* shipped primitives (StatusDot, Badge, Spinner, Menu) inside; pass to
|
|
524
|
+
* AppShell's `utilityBar` slot. Presentation only. */
|
|
525
|
+
export function UtilityBar({
|
|
526
|
+
start,
|
|
527
|
+
end,
|
|
528
|
+
className,
|
|
529
|
+
}: {
|
|
530
|
+
start?: ReactNode;
|
|
531
|
+
end?: ReactNode;
|
|
532
|
+
className?: string;
|
|
533
|
+
}) {
|
|
534
|
+
return (
|
|
535
|
+
<div className={cx("pl-utilitybar", className)}>
|
|
536
|
+
<div className="pl-utilitybar__cluster">{start}</div>
|
|
537
|
+
<div className="pl-utilitybar__spacer" />
|
|
538
|
+
<div className="pl-utilitybar__cluster">{end}</div>
|
|
539
|
+
</div>
|
|
540
|
+
);
|
|
541
|
+
}
|
package/src/forms.tsx
CHANGED
|
@@ -48,27 +48,26 @@ export function Select({ className, children, ...rest }: SelectHTMLAttributes<HT
|
|
|
48
48
|
);
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
/** Toggle switch. Controlled via `checked` / `onCheckedChange`.
|
|
51
|
+
/** Toggle switch. Controlled via `checked` / `onCheckedChange`. Spreads the
|
|
52
|
+
* rest of the input attributes (id, name, aria-*, data-*) onto the inner input —
|
|
53
|
+
* so an external `<label htmlFor>` can associate. */
|
|
52
54
|
export function Switch({
|
|
53
55
|
checked,
|
|
54
56
|
onCheckedChange,
|
|
55
|
-
disabled,
|
|
56
57
|
label,
|
|
57
58
|
className,
|
|
58
|
-
|
|
59
|
-
|
|
59
|
+
...rest
|
|
60
|
+
}: Omit<InputHTMLAttributes<HTMLInputElement>, "onChange" | "type"> & {
|
|
60
61
|
onCheckedChange?: (checked: boolean) => void;
|
|
61
|
-
disabled?: boolean;
|
|
62
62
|
label?: ReactNode;
|
|
63
|
-
className?: string;
|
|
64
63
|
}) {
|
|
65
64
|
return (
|
|
66
|
-
<label className={cx("pl-switch", disabled && "pl-switch--disabled", className)}>
|
|
65
|
+
<label className={cx("pl-switch", rest.disabled && "pl-switch--disabled", className)}>
|
|
67
66
|
<input
|
|
68
67
|
type="checkbox"
|
|
68
|
+
{...rest}
|
|
69
69
|
className="pl-switch__input"
|
|
70
70
|
checked={checked}
|
|
71
|
-
disabled={disabled}
|
|
72
71
|
onChange={(e) => onCheckedChange?.(e.target.checked)}
|
|
73
72
|
/>
|
|
74
73
|
<span className="pl-switch__track" aria-hidden>
|
|
@@ -79,27 +78,26 @@ export function Switch({
|
|
|
79
78
|
);
|
|
80
79
|
}
|
|
81
80
|
|
|
82
|
-
/** Checkbox. Controlled via `checked` / `onCheckedChange`.
|
|
81
|
+
/** Checkbox. Controlled via `checked` / `onCheckedChange`. Spreads the rest of
|
|
82
|
+
* the input attributes (id, name, aria-*, data-*) onto the inner input, so an
|
|
83
|
+
* external `<label htmlFor>` can associate. */
|
|
83
84
|
export function Checkbox({
|
|
84
85
|
checked,
|
|
85
86
|
onCheckedChange,
|
|
86
|
-
disabled,
|
|
87
87
|
label,
|
|
88
88
|
className,
|
|
89
|
-
|
|
90
|
-
|
|
89
|
+
...rest
|
|
90
|
+
}: Omit<InputHTMLAttributes<HTMLInputElement>, "onChange" | "type"> & {
|
|
91
91
|
onCheckedChange?: (checked: boolean) => void;
|
|
92
|
-
disabled?: boolean;
|
|
93
92
|
label?: ReactNode;
|
|
94
|
-
className?: string;
|
|
95
93
|
}) {
|
|
96
94
|
return (
|
|
97
|
-
<label className={cx("pl-checkbox", disabled && "pl-checkbox--disabled", className)}>
|
|
95
|
+
<label className={cx("pl-checkbox", rest.disabled && "pl-checkbox--disabled", className)}>
|
|
98
96
|
<input
|
|
99
97
|
type="checkbox"
|
|
98
|
+
{...rest}
|
|
100
99
|
className="pl-checkbox__input"
|
|
101
100
|
checked={checked}
|
|
102
|
-
disabled={disabled}
|
|
103
101
|
onChange={(e) => onCheckedChange?.(e.target.checked)}
|
|
104
102
|
/>
|
|
105
103
|
<span className="pl-checkbox__box" aria-hidden />
|
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
|
+
}
|
package/src/primitives.tsx
CHANGED
|
@@ -88,3 +88,69 @@ export function TextLink({
|
|
|
88
88
|
/>
|
|
89
89
|
);
|
|
90
90
|
}
|
|
91
|
+
|
|
92
|
+
/** Identity avatar — an image, or initials derived from `name`. */
|
|
93
|
+
export function Avatar({
|
|
94
|
+
src,
|
|
95
|
+
alt,
|
|
96
|
+
name,
|
|
97
|
+
size = 28,
|
|
98
|
+
className,
|
|
99
|
+
}: {
|
|
100
|
+
src?: string;
|
|
101
|
+
alt?: string;
|
|
102
|
+
name?: string;
|
|
103
|
+
size?: number;
|
|
104
|
+
className?: string;
|
|
105
|
+
}) {
|
|
106
|
+
const initials = name
|
|
107
|
+
? name
|
|
108
|
+
.trim()
|
|
109
|
+
.split(/\s+/)
|
|
110
|
+
.slice(0, 2)
|
|
111
|
+
.map((w) => w[0])
|
|
112
|
+
.join("")
|
|
113
|
+
.toUpperCase()
|
|
114
|
+
: "";
|
|
115
|
+
return (
|
|
116
|
+
<span
|
|
117
|
+
className={cx("pl-avatar", className)}
|
|
118
|
+
style={{ width: size, height: size, fontSize: Math.round(size * 0.38) }}
|
|
119
|
+
role="img"
|
|
120
|
+
aria-label={alt ?? name}
|
|
121
|
+
>
|
|
122
|
+
{src ? (
|
|
123
|
+
<img className="pl-avatar__img" src={src} alt={alt ?? name ?? ""} />
|
|
124
|
+
) : (
|
|
125
|
+
<span aria-hidden>{initials || "?"}</span>
|
|
126
|
+
)}
|
|
127
|
+
</span>
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Overlapping avatar stack — wrap Avatar children. */
|
|
132
|
+
export function AvatarGroup({ className, ...rest }: HTMLAttributes<HTMLDivElement>) {
|
|
133
|
+
return <div className={cx("pl-avatar-group", className)} {...rest} />;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Interactive token / chip. Pass `onRemove` for a removable chip. */
|
|
137
|
+
export function Tag({
|
|
138
|
+
children,
|
|
139
|
+
onRemove,
|
|
140
|
+
className,
|
|
141
|
+
}: {
|
|
142
|
+
children: ReactNode;
|
|
143
|
+
onRemove?: () => void;
|
|
144
|
+
className?: string;
|
|
145
|
+
}) {
|
|
146
|
+
return (
|
|
147
|
+
<span className={cx("pl-tag", className)}>
|
|
148
|
+
<span className="pl-tag__label">{children}</span>
|
|
149
|
+
{onRemove && (
|
|
150
|
+
<button type="button" className="pl-tag__remove" aria-label="Remove" onClick={onRemove}>
|
|
151
|
+
×
|
|
152
|
+
</button>
|
|
153
|
+
)}
|
|
154
|
+
</span>
|
|
155
|
+
);
|
|
156
|
+
}
|
package/src/styles/app-shell.css
CHANGED
|
@@ -160,14 +160,50 @@
|
|
|
160
160
|
}
|
|
161
161
|
|
|
162
162
|
/* ── AppShell (dual-rail + 3-column) ─────────────────────────────────────────── */
|
|
163
|
+
/* Vertical frame: content row (1fr) + optional utility bar (40px). */
|
|
164
|
+
.pl-appshell-frame {
|
|
165
|
+
display: flex;
|
|
166
|
+
flex-direction: column;
|
|
167
|
+
height: 100%;
|
|
168
|
+
min-height: 0;
|
|
169
|
+
background: var(--pl-color-bg);
|
|
170
|
+
color: var(--pl-color-fg);
|
|
171
|
+
}
|
|
163
172
|
.pl-appshell {
|
|
164
173
|
display: flex;
|
|
165
174
|
align-items: stretch;
|
|
166
|
-
|
|
175
|
+
flex: 1 1 auto;
|
|
167
176
|
min-height: 0;
|
|
168
177
|
background: var(--pl-color-bg);
|
|
169
178
|
color: var(--pl-color-fg);
|
|
170
179
|
}
|
|
180
|
+
/* Utility bar — the bottom 40px track (third grid row). Desktop only. */
|
|
181
|
+
.pl-appshell__utility {
|
|
182
|
+
flex: 0 0 40px;
|
|
183
|
+
height: 40px;
|
|
184
|
+
display: flex;
|
|
185
|
+
align-items: center;
|
|
186
|
+
border-top: var(--pl-border-width) solid var(--pl-color-border);
|
|
187
|
+
background: var(--pl-color-bg-raised);
|
|
188
|
+
}
|
|
189
|
+
.pl-utilitybar {
|
|
190
|
+
display: flex;
|
|
191
|
+
align-items: center;
|
|
192
|
+
gap: var(--pl-space-2);
|
|
193
|
+
width: 100%;
|
|
194
|
+
height: 100%;
|
|
195
|
+
padding: 0 var(--pl-space-3);
|
|
196
|
+
font-size: 12px;
|
|
197
|
+
color: var(--pl-color-fg-muted);
|
|
198
|
+
}
|
|
199
|
+
.pl-utilitybar__cluster {
|
|
200
|
+
display: flex;
|
|
201
|
+
align-items: center;
|
|
202
|
+
gap: var(--pl-space-3);
|
|
203
|
+
}
|
|
204
|
+
.pl-utilitybar__spacer {
|
|
205
|
+
flex: 1;
|
|
206
|
+
}
|
|
171
207
|
|
|
172
208
|
.pl-appshell__col {
|
|
173
209
|
display: flex;
|
package/src/styles/overlays.css
CHANGED
|
@@ -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
|
+
}
|
|
@@ -217,3 +217,71 @@
|
|
|
217
217
|
width: 16px;
|
|
218
218
|
height: 16px;
|
|
219
219
|
}
|
|
220
|
+
|
|
221
|
+
/* ── Avatar ──────────────────────────────────────────────────────────────────── */
|
|
222
|
+
.pl-avatar {
|
|
223
|
+
display: inline-flex;
|
|
224
|
+
align-items: center;
|
|
225
|
+
justify-content: center;
|
|
226
|
+
flex-shrink: 0;
|
|
227
|
+
overflow: hidden;
|
|
228
|
+
border-radius: 50%;
|
|
229
|
+
background: var(--pl-color-bg-subtle);
|
|
230
|
+
border: var(--pl-border-width) solid var(--pl-color-border);
|
|
231
|
+
color: var(--pl-color-fg-muted);
|
|
232
|
+
font-family: var(--pl-font-mono);
|
|
233
|
+
font-weight: var(--pl-font-weight-medium);
|
|
234
|
+
line-height: 1;
|
|
235
|
+
user-select: none;
|
|
236
|
+
}
|
|
237
|
+
.pl-avatar__img {
|
|
238
|
+
width: 100%;
|
|
239
|
+
height: 100%;
|
|
240
|
+
object-fit: cover;
|
|
241
|
+
}
|
|
242
|
+
.pl-avatar-group {
|
|
243
|
+
display: inline-flex;
|
|
244
|
+
align-items: center;
|
|
245
|
+
}
|
|
246
|
+
.pl-avatar-group > .pl-avatar {
|
|
247
|
+
margin-left: -8px;
|
|
248
|
+
box-shadow: 0 0 0 2px var(--pl-color-bg);
|
|
249
|
+
}
|
|
250
|
+
.pl-avatar-group > .pl-avatar:first-child {
|
|
251
|
+
margin-left: 0;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/* ── Tag / Chip ──────────────────────────────────────────────────────────────── */
|
|
255
|
+
.pl-tag {
|
|
256
|
+
display: inline-flex;
|
|
257
|
+
align-items: center;
|
|
258
|
+
gap: 4px;
|
|
259
|
+
height: 20px;
|
|
260
|
+
padding: 0 6px;
|
|
261
|
+
font-family: var(--pl-font-mono);
|
|
262
|
+
font-size: 11px;
|
|
263
|
+
color: var(--pl-color-fg);
|
|
264
|
+
background: var(--pl-color-bg-subtle);
|
|
265
|
+
border: var(--pl-border-width) solid var(--pl-color-border);
|
|
266
|
+
border-radius: var(--pl-radius);
|
|
267
|
+
}
|
|
268
|
+
.pl-tag__remove {
|
|
269
|
+
display: inline-flex;
|
|
270
|
+
align-items: center;
|
|
271
|
+
justify-content: center;
|
|
272
|
+
width: 14px;
|
|
273
|
+
height: 14px;
|
|
274
|
+
margin-right: -2px;
|
|
275
|
+
padding: 0;
|
|
276
|
+
font-size: 13px;
|
|
277
|
+
line-height: 1;
|
|
278
|
+
color: var(--pl-color-fg-muted);
|
|
279
|
+
background: none;
|
|
280
|
+
border: none;
|
|
281
|
+
border-radius: 50%;
|
|
282
|
+
cursor: pointer;
|
|
283
|
+
}
|
|
284
|
+
.pl-tag__remove:hover {
|
|
285
|
+
color: var(--pl-color-fg);
|
|
286
|
+
background: var(--pl-color-bg-hover);
|
|
287
|
+
}
|