@brdgapp/ui 0.1.2 → 1.0.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.
Files changed (123) hide show
  1. package/CHANGELOG.md +83 -0
  2. package/README.md +37 -201
  3. package/components/action-menu.d.ts +65 -0
  4. package/components/action-menu.js +48 -0
  5. package/components/avatar.d.ts +50 -0
  6. package/components/avatar.js +91 -0
  7. package/components/badge.d.ts +12 -0
  8. package/components/badge.js +23 -0
  9. package/components/bottom-sheet.d.ts +17 -0
  10. package/components/bottom-sheet.js +12 -0
  11. package/components/brand-mark.d.ts +20 -0
  12. package/components/brand-mark.js +44 -0
  13. package/components/button-group.d.ts +25 -0
  14. package/components/button-group.js +53 -0
  15. package/components/button.d.ts +33 -0
  16. package/components/button.js +82 -0
  17. package/components/card.d.ts +78 -0
  18. package/components/card.js +75 -0
  19. package/components/checkbox.d.ts +22 -0
  20. package/components/checkbox.js +35 -0
  21. package/components/collapsible.d.ts +29 -0
  22. package/components/collapsible.js +38 -0
  23. package/components/column-filter-menu.d.ts +15 -0
  24. package/components/column-filter-menu.js +30 -0
  25. package/components/column-sort-button.d.ts +18 -0
  26. package/components/column-sort-button.js +14 -0
  27. package/components/confirm-dialog.d.ts +20 -0
  28. package/components/confirm-dialog.js +17 -0
  29. package/components/copy-field.d.ts +17 -0
  30. package/components/copy-field.js +36 -0
  31. package/components/data-state.d.ts +44 -0
  32. package/components/data-state.js +25 -0
  33. package/components/data-table.d.ts +65 -0
  34. package/components/data-table.js +84 -0
  35. package/components/dialog.d.ts +19 -0
  36. package/components/dialog.js +10 -0
  37. package/components/drawer.d.ts +21 -0
  38. package/components/drawer.js +17 -0
  39. package/components/expandable-bio.d.ts +8 -0
  40. package/components/expandable-bio.js +26 -0
  41. package/components/field.d.ts +29 -0
  42. package/components/field.js +27 -0
  43. package/components/filter-chip.d.ts +11 -0
  44. package/components/filter-chip.js +8 -0
  45. package/components/filter-pill.d.ts +34 -0
  46. package/components/filter-pill.js +35 -0
  47. package/components/hover-card.d.ts +21 -0
  48. package/components/hover-card.js +20 -0
  49. package/components/icon-button.d.ts +16 -0
  50. package/components/icon-button.js +32 -0
  51. package/components/illustration.d.ts +33 -0
  52. package/components/illustration.js +70 -0
  53. package/components/info-hint.d.ts +29 -0
  54. package/components/info-hint.js +42 -0
  55. package/components/input.d.ts +9 -0
  56. package/components/input.js +15 -0
  57. package/components/network-graph.d.ts +48 -0
  58. package/components/network-graph.js +37 -0
  59. package/components/page-banner.d.ts +20 -0
  60. package/components/page-banner.js +29 -0
  61. package/components/scene.d.ts +12 -0
  62. package/components/scene.js +7 -0
  63. package/components/search-input.d.ts +8 -0
  64. package/components/search-input.js +14 -0
  65. package/components/segmented-control.d.ts +18 -0
  66. package/components/segmented-control.js +14 -0
  67. package/components/skeleton.d.ts +12 -0
  68. package/components/skeleton.js +21 -0
  69. package/components/skip-link.d.ts +8 -0
  70. package/components/skip-link.js +7 -0
  71. package/components/stat-box.d.ts +23 -0
  72. package/components/stat-box.js +29 -0
  73. package/components/switch.d.ts +13 -0
  74. package/components/switch.js +8 -0
  75. package/components/tabs.d.ts +6 -0
  76. package/components/tabs.js +25 -0
  77. package/components/tag-input.d.ts +15 -0
  78. package/components/tag-input.js +40 -0
  79. package/components/tag.d.ts +27 -0
  80. package/components/tag.js +35 -0
  81. package/components/textarea.d.ts +12 -0
  82. package/components/textarea.js +78 -0
  83. package/components/theme-switch.d.ts +6 -0
  84. package/components/theme-switch.js +16 -0
  85. package/components/toast.d.ts +25 -0
  86. package/components/toast.js +44 -0
  87. package/hooks/use-is-mobile.d.ts +2 -0
  88. package/hooks/use-is-mobile.js +16 -0
  89. package/hooks/use-mock-query.d.ts +23 -0
  90. package/hooks/use-mock-query.js +36 -0
  91. package/hooks/use-theme.d.ts +11 -0
  92. package/hooks/use-theme.js +34 -0
  93. package/lib/demo-controls.d.ts +24 -0
  94. package/lib/demo-controls.js +24 -0
  95. package/lib/format.d.ts +17 -0
  96. package/lib/format.js +55 -0
  97. package/lib/layout.d.ts +19 -0
  98. package/lib/layout.js +24 -0
  99. package/lib/motion.d.ts +16 -0
  100. package/lib/motion.js +18 -0
  101. package/lib/theme.d.ts +21 -0
  102. package/lib/theme.js +63 -0
  103. package/lib/utils.d.ts +22 -0
  104. package/lib/utils.js +27 -0
  105. package/package.json +32 -16
  106. package/styles/globals.css +595 -0
  107. package/src/index.ts +0 -18
  108. package/src/network-people-helper/README.md +0 -130
  109. package/src/network-people-helper/api.ts +0 -174
  110. package/src/network-people-helper/contact-avatar.tsx +0 -63
  111. package/src/network-people-helper/domain.ts +0 -14
  112. package/src/network-people-helper/icons.tsx +0 -130
  113. package/src/network-people-helper/index.ts +0 -6
  114. package/src/network-people-helper/network-people-helper.tsx +0 -397
  115. package/src/network-people-helper/network-people-nudge.tsx +0 -98
  116. package/src/network-people-helper/open-helper.ts +0 -22
  117. package/src/network-people-helper/person-menu.tsx +0 -211
  118. package/src/network-people-helper/styles.css +0 -848
  119. package/src/network-people-helper/types.ts +0 -23
  120. package/src/network-people-helper/use-network-people-list.ts +0 -122
  121. package/src/network-people-helper/use-network-preview.ts +0 -43
  122. package/src/provider.tsx +0 -59
  123. package/src/styles.css +0 -1
@@ -0,0 +1,34 @@
1
+ import { type ElementProps } from "@brdgapp/ui/lib/utils";
2
+ type FilterPillOption<T extends string> = {
3
+ value: T;
4
+ label: string;
5
+ };
6
+ /** The menu body shared by FilterPill and ColumnFilterMenu. */
7
+ declare function FilterMenuPopup<T extends string>({ value, options, onChange, width, align, }: {
8
+ value: T;
9
+ options: FilterPillOption<T>[];
10
+ onChange: (value: T) => void;
11
+ width: number;
12
+ align: "start" | "end";
13
+ }): import("react").JSX.Element;
14
+ /** The same choices as FilterMenuPopup, as a bottom sheet: what phones get below 768px. */
15
+ declare function FilterSheet<T extends string>({ open, onClose, title, value, options, onChange, }: {
16
+ open: boolean;
17
+ onClose: () => void;
18
+ title: string;
19
+ value: T;
20
+ options: FilterPillOption<T>[];
21
+ onChange: (value: T) => void;
22
+ }): import("react").JSX.Element;
23
+ type FilterPillProps<T extends string> = ElementProps<HTMLButtonElement> & {
24
+ value: T;
25
+ /** The first option is the resting "All" state. */
26
+ options: FilterPillOption<T>[];
27
+ onChange: (value: T) => void;
28
+ /** What is being filtered: "Degree", "Status". Read as "Degree: 1st". */
29
+ ariaLabel?: string;
30
+ className?: string;
31
+ };
32
+ declare function FilterPill<T extends string>({ value, options, onChange, ariaLabel, className, ...rest }: FilterPillProps<T>): import("react").JSX.Element;
33
+ export { FilterPill, FilterMenuPopup, FilterSheet };
34
+ export type { FilterPillOption, FilterPillProps };
@@ -0,0 +1,35 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { useState } from "react";
4
+ import { Menu } from "@base-ui/react/menu";
5
+ import { Check, ChevronDown } from "lucide-react";
6
+ import { BottomSheet, BottomSheetOption } from "@brdgapp/ui/components/bottom-sheet";
7
+ import { useIsMobile } from "@brdgapp/ui/hooks/use-is-mobile";
8
+ import { cn } from "@brdgapp/ui/lib/utils";
9
+ /** The menu body shared by FilterPill and ColumnFilterMenu. */
10
+ function FilterMenuPopup({ value, options, onChange, width, align, }) {
11
+ return (_jsx(Menu.Portal, { children: _jsx(Menu.Positioner, { side: "bottom", align: align, sideOffset: 6, collisionPadding: 8, className: "z-[80]", children: _jsx(Menu.Popup, { "data-slot": "filter-menu", style: { minWidth: width }, className: "max-h-[320px] overflow-y-auto rounded-xl border border-[var(--border-default)] bg-[var(--surface-elevated)] py-1 shadow-[var(--shadow-menu)] outline-none transition-opacity duration-200 ease-out data-[ending-style]:duration-150 data-[starting-style]:opacity-0 data-[ending-style]:opacity-0", children: _jsx(Menu.RadioGroup, { value: value, onValueChange: (v) => onChange(v), children: options.map((option) => (_jsxs(Menu.RadioItem, { value: option.value, closeOnClick: true, "data-slot": "filter-menu-option", className: "group flex w-full cursor-default items-center justify-between gap-3 px-3 py-2 text-left text-[13px] text-[var(--text-body)] outline-none transition-colors data-[checked]:font-semibold data-[checked]:text-[var(--text-primary)] data-[highlighted]:bg-[var(--surface-secondary)] hover:bg-[var(--surface-secondary)]", children: [_jsx("span", { className: "truncate", children: option.label }), _jsx(Menu.RadioItemIndicator, { className: "inline-flex shrink-0", children: _jsx(Check, { "aria-hidden": "true", className: "size-4 text-[var(--primary)]" }) })] }, option.value))) }) }) }) }));
12
+ }
13
+ /** The same choices as FilterMenuPopup, as a bottom sheet: what phones get below 768px. */
14
+ function FilterSheet({ open, onClose, title, value, options, onChange, }) {
15
+ return (_jsx(BottomSheet, { open: open, onClose: onClose, title: title, children: _jsx("div", { role: "radiogroup", "aria-label": title, "data-slot": "filter-sheet", children: options.map((option) => (_jsx(BottomSheetOption, { selected: option.value === value, onSelect: () => {
16
+ onChange(option.value);
17
+ onClose();
18
+ }, children: option.label }, option.value))) }) }));
19
+ }
20
+ function FilterPill({ value, options, onChange, ariaLabel = "Filter", className, ...rest }) {
21
+ const [open, setOpen] = useState(false);
22
+ const mobile = useIsMobile();
23
+ const active = value !== options[0]?.value;
24
+ const label = options.find((o) => o.value === value)?.label ?? options[0]?.label ?? "";
25
+ const triggerClass = cn("inline-flex shrink-0 items-center gap-1.5 rounded-full border px-3 py-1.5 text-[13px] font-semibold outline-none transition-colors", "focus-visible:ring-2 focus-visible:ring-[var(--primary)]", active
26
+ ? "border-[var(--border-hover)] bg-[var(--surface-tertiary)] text-[var(--text-primary)]"
27
+ : "border-[var(--border-default)] bg-transparent text-[var(--text-muted)] hover:text-[var(--text-body)]", className);
28
+ const inner = (_jsxs(_Fragment, { children: [label, _jsx(ChevronDown, { "aria-hidden": "true", className: cn("size-3.5 text-[var(--text-muted)] transition-transform", open && "rotate-180") })] }));
29
+ // Phones: the choices slide up from the bottom, near the thumb, in 44px rows.
30
+ if (mobile) {
31
+ return (_jsxs(_Fragment, { children: [_jsx("button", { ...rest, type: "button", "data-slot": "filter-pill", "aria-haspopup": "dialog", "aria-label": `${ariaLabel}: ${label}`, className: triggerClass, onClick: () => setOpen(true), children: inner }), _jsx(FilterSheet, { open: open, onClose: () => setOpen(false), title: ariaLabel, value: value, options: options, onChange: onChange })] }));
32
+ }
33
+ return (_jsxs(Menu.Root, { open: open, onOpenChange: setOpen, children: [_jsx(Menu.Trigger, { ...rest, "data-slot": "filter-pill", "aria-label": `${ariaLabel}: ${label}`, className: triggerClass, children: inner }), _jsx(FilterMenuPopup, { value: value, options: options, onChange: onChange, width: 200, align: "end" })] }));
34
+ }
35
+ export { FilterPill, FilterMenuPopup, FilterSheet };
@@ -0,0 +1,21 @@
1
+ import { PreviewCard as PreviewCardPrimitive } from "@base-ui/react/preview-card";
2
+ type HoverCardProps = PreviewCardPrimitive.Root.Props;
3
+ /** Holds the trigger and the card. */
4
+ declare function HoverCard(props: HoverCardProps): import("react").JSX.Element;
5
+ type HoverCardTriggerProps = Omit<PreviewCardPrimitive.Trigger.Props, "className"> & {
6
+ /** Layout only. The trigger is usually a person's name or a logo, styled where it's used. */
7
+ className?: string;
8
+ };
9
+ /**
10
+ * What people hover: a name or a logo. A link by default (`href` to the full profile); pass `render` to
11
+ * use your router's link or a button instead.
12
+ */
13
+ declare function HoverCardTrigger({ delay, closeDelay, className, ...props }: HoverCardTriggerProps): import("react").JSX.Element;
14
+ type HoverCardContentProps = Omit<PreviewCardPrimitive.Popup.Props, "className"> & Pick<PreviewCardPrimitive.Positioner.Props, "side" | "align"> & {
15
+ /** Width only: `w-72`, `w-[374px]`. 320px by default. */
16
+ className?: string;
17
+ };
18
+ /** The card. Read-only: what something is, never buttons. */
19
+ declare function HoverCardContent({ side, align, className, ...props }: HoverCardContentProps): import("react").JSX.Element;
20
+ export { HoverCard, HoverCardContent, HoverCardTrigger };
21
+ export type { HoverCardContentProps, HoverCardProps, HoverCardTriggerProps };
@@ -0,0 +1,20 @@
1
+ "use client";
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { PreviewCard as PreviewCardPrimitive } from "@base-ui/react/preview-card";
4
+ import { cn } from "@brdgapp/ui/lib/utils";
5
+ /** Holds the trigger and the card. */
6
+ function HoverCard(props) {
7
+ return _jsx(PreviewCardPrimitive.Root, { "data-slot": "hover-card", ...props });
8
+ }
9
+ /**
10
+ * What people hover: a name or a logo. A link by default (`href` to the full profile); pass `render` to
11
+ * use your router's link or a button instead.
12
+ */
13
+ function HoverCardTrigger({ delay = 300, closeDelay = 150, className, ...props }) {
14
+ return (_jsx(PreviewCardPrimitive.Trigger, { "data-slot": "hover-card-trigger", delay: delay, closeDelay: closeDelay, className: cn("rounded-sm outline-none focus-visible:ring-2 focus-visible:ring-[var(--primary)]", className), ...props }));
15
+ }
16
+ /** The card. Read-only: what something is, never buttons. */
17
+ function HoverCardContent({ side = "bottom", align = "start", className, ...props }) {
18
+ return (_jsx(PreviewCardPrimitive.Portal, { children: _jsx(PreviewCardPrimitive.Positioner, { side: side, align: align, sideOffset: 6, collisionPadding: 12, className: "z-[80]", children: _jsx(PreviewCardPrimitive.Popup, { "data-slot": "hover-card-content", className: cn("w-80 max-w-[calc(100vw-24px)] origin-[var(--transform-origin)] rounded-xl border border-[var(--border-default)] bg-[var(--surface-elevated)] p-3 text-[13px] text-[var(--text-body)] shadow-[var(--shadow-dropdown)] outline-none", "transition-[opacity,scale] duration-200 ease-out data-[ending-style]:duration-150 motion-reduce:transition-none", "data-[starting-style]:scale-95 data-[starting-style]:opacity-0 data-[ending-style]:scale-95 data-[ending-style]:opacity-0", className), ...props }) }) }));
19
+ }
20
+ export { HoverCard, HoverCardContent, HoverCardTrigger };
@@ -0,0 +1,16 @@
1
+ import { Button as ButtonPrimitive } from "@base-ui/react/button";
2
+ import { type VariantProps } from "class-variance-authority";
3
+ declare const iconButtonVariants: (props?: ({
4
+ variant?: "default" | "danger" | null | undefined;
5
+ size?: "sm" | "md" | "lg" | null | undefined;
6
+ } & import("class-variance-authority/types").ClassProp) | undefined) => string;
7
+ type IconButtonProps = Omit<ButtonPrimitive.Props, "className" | "aria-label"> & VariantProps<typeof iconButtonVariants> & {
8
+ /** Required: an icon has no visible name, so this is what screen readers say. Name the action: "Close". */
9
+ "aria-label": string;
10
+ /** The action is running: shows a spinner, disables the button, sets `aria-busy`. */
11
+ loading?: boolean;
12
+ className?: string;
13
+ };
14
+ declare function IconButton({ className, variant, size, loading, disabled, children, ...props }: IconButtonProps): import("react").JSX.Element;
15
+ export { IconButton, iconButtonVariants };
16
+ export type { IconButtonProps };
@@ -0,0 +1,32 @@
1
+ "use client";
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { Button as ButtonPrimitive } from "@base-ui/react/button";
4
+ import { cva } from "class-variance-authority";
5
+ import { Loader2 } from "lucide-react";
6
+ import { cn } from "@brdgapp/ui/lib/utils";
7
+ // The one icon-only button: close, kebab, back, copy, sync. Always round. Ported from Bridge OS
8
+ // src/components/ui/icon-button.tsx onto Base UI Button. All sizes clear the 24px minimum target.
9
+ const iconButtonVariants = cva([
10
+ "inline-grid shrink-0 place-items-center rounded-full border border-transparent",
11
+ "transition-colors duration-150 ease-out outline-none",
12
+ "focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--surface-primary)]",
13
+ "disabled:pointer-events-none disabled:opacity-50",
14
+ "[&_svg]:size-4",
15
+ ], {
16
+ variants: {
17
+ variant: {
18
+ default: "text-[var(--text-muted)] hover:bg-[var(--surface-secondary)] hover:text-[var(--text-primary)] focus-visible:ring-[var(--primary)]",
19
+ danger: "text-[var(--text-muted)] hover:bg-[var(--error-bg)] hover:text-[var(--danger-text)] focus-visible:ring-[var(--error)]",
20
+ },
21
+ size: {
22
+ sm: "size-7",
23
+ md: "size-8",
24
+ lg: "size-9",
25
+ },
26
+ },
27
+ defaultVariants: { variant: "default", size: "md" },
28
+ });
29
+ function IconButton({ className, variant, size, loading = false, disabled, children, ...props }) {
30
+ return (_jsx(ButtonPrimitive, { "data-slot": "icon-button", className: cn(iconButtonVariants({ variant, size }), className), disabled: disabled || loading, "aria-busy": loading || undefined, ...props, children: _jsx("span", { "aria-hidden": "true", className: "inline-grid place-items-center", children: loading ? _jsx(Loader2, { className: "animate-spin" }) : children }) }));
31
+ }
32
+ export { IconButton, iconButtonVariants };
@@ -0,0 +1,33 @@
1
+ declare const DRAWINGS: {
2
+ readonly people: () => import("react").JSX.Element;
3
+ readonly intros: () => import("react").JSX.Element;
4
+ readonly companies: () => import("react").JSX.Element;
5
+ readonly messages: () => import("react").JSX.Element;
6
+ readonly activity: () => import("react").JSX.Element;
7
+ readonly meetings: () => import("react").JSX.Element;
8
+ readonly lists: () => import("react").JSX.Element;
9
+ readonly jobs: () => import("react").JSX.Element;
10
+ readonly connect: () => import("react").JSX.Element;
11
+ readonly search: () => import("react").JSX.Element;
12
+ readonly filters: () => import("react").JSX.Element;
13
+ readonly done: () => import("react").JSX.Element;
14
+ readonly error: () => import("react").JSX.Element;
15
+ };
16
+ export type IllustrationName = keyof typeof DRAWINGS;
17
+ export declare const ILLUSTRATION_NAMES: IllustrationName[];
18
+ declare const SIZE: {
19
+ readonly sm: "w-20";
20
+ readonly md: "w-30";
21
+ readonly lg: "w-40";
22
+ };
23
+ type IllustrationProps = {
24
+ name: IllustrationName;
25
+ /** 80px wide in cards and side panels, 120px for a page's empty state, 160px for a screen with nothing else on it. */
26
+ size?: keyof typeof SIZE;
27
+ /** Only when the picture says something the words beside it don't. Usually leave it off: it's decoration. */
28
+ title?: string;
29
+ className?: string;
30
+ };
31
+ /** One of Bridge's drawings for empty states. Usually through DataState's `illustration`. */
32
+ declare function Illustration({ name, size, title, className }: IllustrationProps): import("react").JSX.Element;
33
+ export { Illustration };
@@ -0,0 +1,70 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { useId } from "react";
4
+ import { cn } from "@brdgapp/ui/lib/utils";
5
+ // Bridge's drawings for empty states: tiny scenes made of Bridge's own UI pieces (cards, faces, lines for
6
+ // text, one blue button or mark) on a patch of the dot grid. Style chosen by Aman 2026-11-03 ("mini product
7
+ // scene"), after the feature-card pictures in Bridge OS's profile tab. Subjects from the empty states across
8
+ // all 12 apps (people, intros, companies, messages, activity, meetings, lists, jobs, sync; search, filters,
9
+ // done, error). Every color is a token, so a drawing follows light and dark with no second file.
10
+ // ---- Pieces ----------------------------------------------------------------------------------------------
11
+ const CARD = "fill-[var(--surface-elevated)] stroke-[var(--border-default)]";
12
+ const SOFT = "fill-[var(--surface-secondary)] stroke-[var(--border-default)]";
13
+ const FACE = {
14
+ blue: ["fill-[var(--info-bg)]", "fill-[var(--info)]"],
15
+ purple: ["fill-[var(--purple-bg)]", "fill-[var(--purple)]"],
16
+ green: ["fill-[var(--success-bg)]", "fill-[var(--success)]"],
17
+ };
18
+ function Card({ x, y, w, h, r = 7, back = false, className }) {
19
+ return _jsx("rect", { x: x, y: y, width: w, height: h, rx: r, vectorEffect: "non-scaling-stroke", className: cn(CARD, back && "opacity-60", className) });
20
+ }
21
+ /** A face in a circle: head and shoulders, in one of the avatar tints. */
22
+ function Face({ cx, cy, r = 8, tone = "blue" }) {
23
+ const [bg, fg] = FACE[tone];
24
+ const s = r * 0.62;
25
+ return (_jsxs("g", { children: [_jsx("circle", { cx: cx, cy: cy, r: r, className: bg }), _jsx("circle", { cx: cx, cy: cy - r * 0.22, r: r * 0.34, className: fg }), _jsx("path", { d: `M${cx - s} ${cy + r * 0.62}a${s} ${s * 0.85} 0 0 1 ${2 * s} 0z`, className: fg })] }));
26
+ }
27
+ /** A line of text. `strong` for a name or title, plain for the line under it. */
28
+ function Line({ x, y, w, strong = false }) {
29
+ return _jsx("rect", { x: x, y: y, width: w, height: 4, rx: 2, className: strong ? "fill-[var(--text-disabled)] opacity-60" : "fill-[var(--border-default)]" });
30
+ }
31
+ /** The one blue thing in a drawing: a button. */
32
+ function Pill({ x, y, w, h = 9 }) {
33
+ return _jsx("rect", { x: x, y: y, width: w, height: h, rx: h / 2, className: "fill-[var(--primary)]" });
34
+ }
35
+ function Chip({ x, y, w }) {
36
+ return _jsx("rect", { x: x, y: y, width: w, height: 9, rx: 4.5, vectorEffect: "non-scaling-stroke", className: SOFT });
37
+ }
38
+ /** A place where something would be, but isn't. */
39
+ function Empty({ x, y, w, h, r = 6 }) {
40
+ return _jsx("rect", { x: x, y: y, width: w, height: h, rx: r, fill: "none", strokeDasharray: "3 3", vectorEffect: "non-scaling-stroke", className: "stroke-[var(--text-disabled)] opacity-60" });
41
+ }
42
+ const MARK = "stroke-[var(--primary)]";
43
+ const WHITE = "stroke-white";
44
+ // ---- Drawings (120 × 96) ------------------------------------------------------------------------------
45
+ const DRAWINGS = {
46
+ people: () => (_jsxs(_Fragment, { children: [_jsx(Card, { x: 38, y: 16, w: 58, h: 34, back: true }), _jsx(Card, { x: 22, y: 32, w: 66, h: 46, r: 8 }), _jsx(Face, { cx: 37, cy: 47 }), _jsx(Line, { x: 50, y: 42, w: 28, strong: true }), _jsx(Line, { x: 50, y: 50, w: 18 }), _jsx(Pill, { x: 30, y: 63, w: 30 })] })),
47
+ intros: () => (_jsxs(_Fragment, { children: [_jsx(Face, { cx: 30, cy: 44, r: 13 }), _jsx(Face, { cx: 90, cy: 44, r: 13, tone: "purple" }), _jsx("line", { x1: 46, y1: 44, x2: 74, y2: 44, strokeWidth: 1.5, strokeDasharray: "3 3", className: "stroke-[var(--text-disabled)]" }), _jsx(Pill, { x: 45, y: 64, w: 30 })] })),
48
+ companies: () => (_jsxs(_Fragment, { children: [_jsx(Card, { x: 38, y: 14, w: 58, h: 32, back: true }), _jsx(Card, { x: 22, y: 30, w: 68, h: 48, r: 8 }), _jsx("rect", { x: 30, y: 38, width: 16, height: 16, rx: 4, vectorEffect: "non-scaling-stroke", className: SOFT }), _jsx("rect", { x: 34, y: 46, width: 2.5, height: 5, rx: 0.5, className: "fill-[var(--text-disabled)]" }), _jsx("rect", { x: 37.5, y: 42, width: 2.5, height: 9, rx: 0.5, className: "fill-[var(--text-disabled)]" }), _jsx("rect", { x: 41, y: 44, width: 2.5, height: 7, rx: 0.5, className: "fill-[var(--text-disabled)]" }), _jsx(Line, { x: 52, y: 40, w: 30, strong: true }), _jsx(Line, { x: 52, y: 48, w: 20 }), _jsx(Chip, { x: 30, y: 63, w: 22 }), _jsx(Pill, { x: 56, y: 63, w: 24 })] })),
49
+ messages: () => (_jsxs(_Fragment, { children: [_jsx(Card, { x: 16, y: 20, w: 58, h: 26, r: 9 }), _jsx(Line, { x: 24, y: 28, w: 36, strong: true }), _jsx(Line, { x: 24, y: 36, w: 24 }), _jsx("rect", { x: 46, y: 54, width: 58, height: 24, rx: 9, className: "fill-[var(--primary)]" }), _jsx("rect", { x: 54, y: 62, width: 34, height: 4, rx: 2, className: "fill-white opacity-80" }), _jsx("rect", { x: 54, y: 69, width: 20, height: 4, rx: 2, className: "fill-white opacity-50" })] })),
50
+ activity: () => (_jsxs(_Fragment, { children: [_jsx(Card, { x: 18, y: 14, w: 84, h: 20, r: 6 }), _jsx(Face, { cx: 29, cy: 24, r: 6 }), _jsx(Line, { x: 40, y: 20, w: 34, strong: true }), _jsx(Line, { x: 40, y: 27, w: 22 }), _jsx("circle", { cx: 93, cy: 24, r: 2.8, className: "fill-[var(--primary)]" }), _jsx(Card, { x: 18, y: 38, w: 84, h: 20, r: 6 }), _jsx(Face, { cx: 29, cy: 48, r: 6, tone: "purple" }), _jsx(Line, { x: 40, y: 44, w: 28, strong: true }), _jsx(Line, { x: 40, y: 51, w: 18 }), _jsx(Card, { x: 18, y: 62, w: 84, h: 20, r: 6, back: true }), _jsx(Face, { cx: 29, cy: 72, r: 6, tone: "green" }), _jsx(Line, { x: 40, y: 68, w: 30, strong: true }), _jsx(Line, { x: 40, y: 75, w: 20 })] })),
51
+ meetings: () => (_jsxs(_Fragment, { children: [_jsx(Card, { x: 24, y: 14, w: 72, h: 68, r: 8 }), _jsx(Line, { x: 32, y: 22, w: 26, strong: true }), [0, 1, 2, 3].flatMap((row) => [0, 1, 2, 3, 4].map((col) => {
52
+ const on = row === 1 && col === 2;
53
+ return _jsx("rect", { x: 32 + col * 12, y: 34 + row * 11, width: 9, height: 7, rx: 2, className: on ? "fill-[var(--primary)]" : "fill-[var(--surface-secondary)]" }, `${row}-${col}`);
54
+ }))] })),
55
+ lists: () => (_jsxs(_Fragment, { children: [_jsx(Card, { x: 34, y: 14, w: 60, h: 40, back: true }), _jsx(Card, { x: 24, y: 24, w: 64, h: 54, r: 8 }), [0, 1, 2].map((j) => (_jsxs("g", { children: [_jsx("rect", { x: 32, y: 34 + j * 13, width: 7, height: 7, rx: 2, vectorEffect: "non-scaling-stroke", className: SOFT }), _jsx(Line, { x: 44, y: 35.5 + j * 13, w: 30 - j * 6, strong: j === 0 })] }, j))), _jsx("path", { d: "M76 24h8v13l-4-3-4 3z", className: "fill-[var(--primary)]" })] })),
56
+ jobs: () => (_jsxs(_Fragment, { children: [_jsx(Card, { x: 22, y: 20, w: 76, h: 58, r: 8 }), _jsx("rect", { x: 30, y: 28, width: 16, height: 16, rx: 4, vectorEffect: "non-scaling-stroke", className: SOFT }), _jsx("rect", { x: 33.5, y: 34, width: 9, height: 7, rx: 1.5, fill: "none", vectorEffect: "non-scaling-stroke", className: "stroke-[var(--text-disabled)]" }), _jsx("path", { d: "M36 34v-1.5h4V34", fill: "none", vectorEffect: "non-scaling-stroke", className: "stroke-[var(--text-disabled)]" }), _jsx(Line, { x: 52, y: 30, w: 34, strong: true }), _jsx(Line, { x: 52, y: 38, w: 22 }), _jsx(Chip, { x: 30, y: 52, w: 20 }), _jsx(Chip, { x: 54, y: 52, w: 20 }), _jsx(Pill, { x: 30, y: 64, w: 28 })] })),
57
+ connect: () => (_jsxs(_Fragment, { children: [_jsx(Card, { x: 12, y: 30, w: 32, h: 32, r: 8 }), _jsx("rect", { x: 19, y: 38, width: 18, height: 16, rx: 2, fill: "none", vectorEffect: "non-scaling-stroke", className: "stroke-[var(--text-disabled)]" }), _jsx("line", { x1: 19, y1: 42, x2: 37, y2: 42, vectorEffect: "non-scaling-stroke", className: "stroke-[var(--text-disabled)]" }), _jsx(Card, { x: 76, y: 30, w: 32, h: 32, r: 8 }), _jsx(Face, { cx: 92, cy: 46, r: 9 }), _jsx("line", { x1: 44, y1: 46, x2: 76, y2: 46, strokeWidth: 1.5, strokeDasharray: "3 3", className: "stroke-[var(--text-disabled)]" }), _jsx("circle", { cx: 60, cy: 46, r: 8, className: "fill-[var(--primary)]" }), _jsx("path", { d: "M56 44h7l-2-2M64 48h-7l2 2", fill: "none", strokeWidth: 1.4, strokeLinecap: "round", strokeLinejoin: "round", className: WHITE })] })),
58
+ search: () => (_jsxs(_Fragment, { children: [_jsx("rect", { x: 16, y: 16, width: 88, height: 18, rx: 9, vectorEffect: "non-scaling-stroke", className: CARD }), _jsx("circle", { cx: 27, cy: 24.5, r: 3.6, fill: "none", strokeWidth: 1.6, className: MARK }), _jsx("line", { x1: 29.7, y1: 27.2, x2: 32.5, y2: 30, strokeWidth: 1.6, strokeLinecap: "round", className: MARK }), _jsx(Line, { x: 38, y: 23, w: 30 }), _jsx(Empty, { x: 16, y: 42, w: 88, h: 16 }), _jsx(Empty, { x: 16, y: 64, w: 88, h: 16 })] })),
59
+ filters: () => (_jsxs(_Fragment, { children: [_jsx(Chip, { x: 16, y: 16, w: 24 }), _jsx("rect", { x: 44, y: 16, width: 30, height: 9, rx: 4.5, strokeWidth: 1.4, className: "fill-[var(--info-bg)] stroke-[var(--primary)]" }), _jsx(Chip, { x: 78, y: 16, w: 26 }), _jsx(Empty, { x: 16, y: 36, w: 88, h: 18 }), _jsx(Empty, { x: 16, y: 60, w: 88, h: 18 })] })),
60
+ done: () => (_jsxs(_Fragment, { children: [_jsx(Card, { x: 22, y: 20, w: 68, h: 58, r: 8 }), [0, 1, 2].map((j) => (_jsxs("g", { children: [_jsx("circle", { cx: 33, cy: 34 + j * 14, r: 4, className: "fill-[var(--surface-secondary)]" }), _jsx("path", { d: `M31 ${34 + j * 14}l1.4 1.4 2.6-2.8`, fill: "none", strokeWidth: 1.2, strokeLinecap: "round", strokeLinejoin: "round", className: "stroke-[var(--text-disabled)]" }), _jsx(Line, { x: 42, y: 32 + j * 14, w: 36 - j * 8, strong: j === 0 })] }, j))), _jsx("circle", { cx: 88, cy: 22, r: 10, className: "fill-[var(--primary)]" }), _jsx("path", { d: "M83.5 22l3 3 5.5-6", fill: "none", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round", className: WHITE })] })),
61
+ error: () => (_jsxs(_Fragment, { children: [_jsx(Card, { x: 22, y: 16, w: 76, h: 46, r: 8 }), _jsx(Line, { x: 32, y: 26, w: 40, strong: true }), _jsx(Line, { x: 32, y: 36, w: 20 }), _jsx(Line, { x: 60, y: 36, w: 26 }), _jsx(Line, { x: 32, y: 46, w: 14 }), _jsx(Line, { x: 52, y: 46, w: 10 }), _jsx("circle", { cx: 86, cy: 27, r: 6, className: "fill-[var(--surface-secondary)]" }), _jsx("rect", { x: 85.2, y: 23.5, width: 1.6, height: 4.5, rx: 0.8, className: "fill-[var(--text-muted)]" }), _jsx("circle", { cx: 86, cy: 30.3, r: 0.9, className: "fill-[var(--text-muted)]" }), _jsx(Pill, { x: 42, y: 70, w: 36 })] })),
62
+ };
63
+ export const ILLUSTRATION_NAMES = Object.keys(DRAWINGS);
64
+ const SIZE = { sm: "w-20", md: "w-30", lg: "w-40" };
65
+ /** One of Bridge's drawings for empty states. Usually through DataState's `illustration`. */
66
+ function Illustration({ name, size = "md", title, className }) {
67
+ const grid = useId();
68
+ return (_jsxs("svg", { "data-slot": "illustration", viewBox: "0 0 120 96", role: title ? "img" : undefined, "aria-label": title, "aria-hidden": title ? undefined : true, focusable: "false", className: cn("aspect-[5/4] h-auto shrink-0", SIZE[size], className), children: [_jsx("defs", { children: _jsx("pattern", { id: grid, width: 8, height: 8, patternUnits: "userSpaceOnUse", children: _jsx("circle", { cx: 4, cy: 4, r: 0.8, className: "fill-[var(--primary)] opacity-20" }) }) }), _jsx("rect", { x: 4, y: 4, width: 112, height: 88, rx: 12, fill: `url(#${grid})` }), DRAWINGS[name]()] }));
69
+ }
70
+ export { Illustration };
@@ -0,0 +1,29 @@
1
+ import type * as React from "react";
2
+ import { type ElementProps } from "@brdgapp/ui/lib/utils";
3
+ /** An ⓘ that explains the thing next to it. Opens on hover, focus or tap. */
4
+ declare function InfoHint({ text, className, ...rest }: {
5
+ text: string;
6
+ className?: string;
7
+ } & ElementProps<HTMLButtonElement>): React.JSX.Element;
8
+ /**
9
+ * Wraps content that isn't already a control (a chip, a count) in a button with a tooltip.
10
+ * The content itself must say what it is; the tooltip only adds detail.
11
+ */
12
+ declare function HoverTip({ text, children, className, ...rest }: {
13
+ text: string;
14
+ children: React.ReactNode;
15
+ className?: string;
16
+ } & ElementProps<HTMLButtonElement>): React.JSX.Element;
17
+ /**
18
+ * A tooltip for something that is already a control (a sort button) or a plain label. The child
19
+ * becomes the trigger, so no button is nested inside another. `focusable` makes a plain label a tab
20
+ * stop; `disabled` hides the tip (for example while the control's own menu is open).
21
+ */
22
+ declare function Tip({ text, children, disabled, focusable, className, }: {
23
+ text?: string;
24
+ children: React.ReactNode;
25
+ disabled?: boolean;
26
+ focusable?: boolean;
27
+ className?: string;
28
+ }): React.JSX.Element;
29
+ export { InfoHint, HoverTip, Tip };
@@ -0,0 +1,42 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { isValidElement } from "react";
4
+ import { Popover } from "@base-ui/react/popover";
5
+ import { Tooltip } from "@base-ui/react/tooltip";
6
+ import { Info } from "lucide-react";
7
+ import { cn } from "@brdgapp/ui/lib/utils";
8
+ // Bridge's explain-this bubbles, ported from Bridge OS src/components/ui/info-hint.tsx (same API).
9
+ // Bridge OS hand-built each one (portal + measured position + clamping). They now sit on Base UI:
10
+ // InfoHint → Popover that opens on hover AND on tap, so touch and screen-reader users can read it
11
+ // (Base UI: "popups that open when hovering an info icon should use Popover").
12
+ // HoverTip, Tip → Tooltip: a visual extra for mouse and keyboard users; the control keeps its own name.
13
+ // All render outside the page (portal), so a glass card's overflow can't clip them, and they shift to
14
+ // stay on screen. They open instantly, as in Bridge OS.
15
+ const BUBBLE = "z-[80] w-max max-w-[min(16rem,calc(100vw-24px))] rounded-lg border border-[var(--border-default)] bg-[var(--surface-elevated)] px-3 py-2 text-[12px] leading-snug text-[var(--text-body)] shadow-[var(--shadow-dropdown)] outline-none " +
16
+ "transition-opacity duration-200 ease-out data-[ending-style]:duration-150 data-[starting-style]:opacity-0 data-[ending-style]:opacity-0";
17
+ /** An ⓘ that explains the thing next to it. Opens on hover, focus or tap. */
18
+ function InfoHint({ text, className, ...rest }) {
19
+ return (_jsxs(Popover.Root, { children: [_jsx(Popover.Trigger, { ...rest, openOnHover: true, delay: 0, "data-slot": "info-hint", "aria-label": text, className: cn("inline-flex shrink-0 rounded-full text-[var(--text-disabled)] outline-none transition-colors hover:text-[var(--text-muted)]", "focus-visible:text-[var(--text-muted)] focus-visible:ring-2 focus-visible:ring-[var(--primary)]", className), children: _jsx(Info, { "aria-hidden": "true", className: "size-3.5" }) }), _jsx(Popover.Portal, { children: _jsx(Popover.Positioner, { side: "bottom", sideOffset: 6, collisionPadding: 12, className: "z-[80]", children: _jsx(Popover.Popup, { "data-slot": "info-hint-popup", className: cn(BUBBLE, "w-60"), children: text }) }) })] }));
20
+ }
21
+ function TipBubble({ text }) {
22
+ return (_jsx(Tooltip.Portal, { children: _jsx(Tooltip.Positioner, { side: "bottom", sideOffset: 6, collisionPadding: 12, className: "z-[80]", children: _jsx(Tooltip.Popup, { "data-slot": "tip-popup", className: BUBBLE, children: text }) }) }));
23
+ }
24
+ /**
25
+ * Wraps content that isn't already a control (a chip, a count) in a button with a tooltip.
26
+ * The content itself must say what it is; the tooltip only adds detail.
27
+ */
28
+ function HoverTip({ text, children, className, ...rest }) {
29
+ return (_jsxs(Tooltip.Root, { children: [_jsx(Tooltip.Trigger, { ...rest, delay: 0, "data-slot": "hover-tip", className: cn("outline-none focus-visible:ring-2 focus-visible:ring-[var(--primary)] rounded", className), children: children }), _jsx(TipBubble, { text: text })] }));
30
+ }
31
+ /**
32
+ * A tooltip for something that is already a control (a sort button) or a plain label. The child
33
+ * becomes the trigger, so no button is nested inside another. `focusable` makes a plain label a tab
34
+ * stop; `disabled` hides the tip (for example while the control's own menu is open).
35
+ */
36
+ function Tip({ text, children, disabled, focusable, className, }) {
37
+ if (!text)
38
+ return _jsx(_Fragment, { children: children });
39
+ const trigger = !focusable && isValidElement(children) ? children : (_jsx("span", { tabIndex: focusable ? 0 : undefined, className: cn("inline-flex", focusable && "rounded outline-none focus-visible:ring-2 focus-visible:ring-[var(--primary)]", className), children: children }));
40
+ return (_jsxs(Tooltip.Root, { disabled: disabled, children: [_jsx(Tooltip.Trigger, { delay: 0, render: trigger, "data-slot": "tip" }), _jsx(TipBubble, { text: text })] }));
41
+ }
42
+ export { InfoHint, HoverTip, Tip };
@@ -0,0 +1,9 @@
1
+ import type * as React from "react";
2
+ import { Input as InputPrimitive } from "@base-ui/react/input";
3
+ type InputProps = Omit<InputPrimitive.Props, "className"> & {
4
+ /** Layout only: `max-w-sm`, `flex-1`. */
5
+ className?: string;
6
+ };
7
+ declare function Input({ className, type, ...props }: InputProps): React.JSX.Element;
8
+ export { Input };
9
+ export type { InputProps };
@@ -0,0 +1,15 @@
1
+ "use client";
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { Input as InputPrimitive } from "@base-ui/react/input";
4
+ import { cn } from "@brdgapp/ui/lib/utils";
5
+ // A one-line text field. Started from shadcn's Base UI input, then put on Bridge's look: Bridge OS's own
6
+ // Input (src/components/ui/input.tsx) and Add a network use a 40px white box with 14px text, 16px on phones
7
+ // so iOS doesn't zoom into the field (picked
8
+ // 2026-11-03 over Edit contact's 36px grey box). Same height as SearchInput, so they line up.
9
+ // Inside a Field, the label, help text and error are tied to it automatically.
10
+ // The placeholder uses --text-muted, not --text-disabled as in Bridge OS: disabled grey is 2:1 on white.
11
+ const inputClasses = cn("h-11 md:h-10 w-full min-w-0 rounded-lg border border-[var(--border-default)] bg-[var(--surface-primary)] px-3.5", "text-16 md:text-14 text-[var(--text-primary)] placeholder:text-[var(--text-muted)]", "transition-colors duration-150 ease-out outline-none", "hover:border-[var(--border-hover)]", "focus-visible:border-[var(--primary)] focus-visible:ring-2 focus-visible:ring-[var(--primary)]/20", "aria-invalid:border-[var(--error)] aria-invalid:hover:border-[var(--error)] aria-invalid:focus-visible:border-[var(--error)] aria-invalid:focus-visible:ring-[var(--error)]/20", "data-[invalid]:border-[var(--error)] data-[invalid]:hover:border-[var(--error)] data-[invalid]:focus-visible:border-[var(--error)] data-[invalid]:focus-visible:ring-[var(--error)]/20", "disabled:cursor-not-allowed disabled:opacity-50");
12
+ function Input({ className, type = "text", ...props }) {
13
+ return _jsx(InputPrimitive, { "data-slot": "input", type: type, className: cn(inputClasses, className), ...props });
14
+ }
15
+ export { Input };
@@ -0,0 +1,48 @@
1
+ type Person = {
2
+ name: string;
3
+ src?: string;
4
+ };
5
+ declare const SIZES: {
6
+ readonly sm: {
7
+ readonly width: "w-[200px]";
8
+ readonly center: "lg";
9
+ readonly inner: "md";
10
+ readonly outer: "sm";
11
+ readonly you: "size-10 text-12";
12
+ };
13
+ readonly md: {
14
+ readonly width: "w-[280px]";
15
+ readonly center: "xl";
16
+ readonly inner: "lg";
17
+ readonly outer: "md";
18
+ readonly you: "size-12 text-13";
19
+ };
20
+ readonly lg: {
21
+ readonly width: "w-[360px]";
22
+ readonly center: "2xl";
23
+ readonly inner: "xl";
24
+ readonly outer: "lg";
25
+ readonly you: "size-16 text-16";
26
+ };
27
+ };
28
+ type NetworkGraphProps = {
29
+ /** The people around the middle. The first 6 sit close; up to 10 more sit further out. */
30
+ people: Person[];
31
+ /** Who is in the middle. Leave it off for "You". */
32
+ center?: Person;
33
+ /** Names whose line shows in blue: an intro path, someone new. */
34
+ highlight?: string[];
35
+ size?: keyof typeof SIZES;
36
+ /** Lines and faces come in one after another when it first shows. Off under reduced motion. */
37
+ animate?: boolean;
38
+ /**
39
+ * Say what the picture shows, for screen readers: "You and 8 people you know". Leave it off when the words
40
+ * beside it already say it; it's then hidden from them.
41
+ */
42
+ label?: string;
43
+ className?: string;
44
+ };
45
+ /** A network: you (or someone) in the middle, people around, a line to each. */
46
+ declare function NetworkGraph({ people, center, highlight, size, animate, label, className }: NetworkGraphProps): import("react").JSX.Element;
47
+ export { NetworkGraph };
48
+ export type { NetworkGraphProps };
@@ -0,0 +1,37 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Avatar } from "@brdgapp/ui/components/avatar";
3
+ import { cn } from "@brdgapp/ui/lib/utils";
4
+ const SIZES = {
5
+ sm: { width: "w-[200px]", center: "lg", inner: "md", outer: "sm", you: "size-10 text-12" },
6
+ md: { width: "w-[280px]", center: "xl", inner: "lg", outer: "md", you: "size-12 text-13" },
7
+ lg: { width: "w-[360px]", center: "2xl", inner: "xl", outer: "lg", you: "size-16 text-16" },
8
+ };
9
+ const INNER = 6;
10
+ const OUTER = 10;
11
+ function place(i, count, radius, offset) {
12
+ const a = offset + (i / count) * Math.PI * 2 - Math.PI / 2;
13
+ return { x: 50 + radius * Math.cos(a), y: 50 + radius * Math.sin(a) };
14
+ }
15
+ /** A network: you (or someone) in the middle, people around, a line to each. */
16
+ function NetworkGraph({ people, center, highlight = [], size = "md", animate = true, label, className }) {
17
+ const s = SIZES[size];
18
+ const inner = people.slice(0, INNER);
19
+ const outer = people.slice(INNER, INNER + OUTER);
20
+ const innerAt = inner.map((_, i) => place(i, inner.length, 30, 0));
21
+ const outerAt = outer.map((_, i) => place(i, outer.length, 45, Math.PI / outer.length));
22
+ const lit = new Set(highlight);
23
+ // Outer people hang off the nearest close-in person, so the picture reads as friends of friends.
24
+ const nearest = (p) => innerAt.reduce((best, q, i) => (Math.hypot(q.x - p.x, q.y - p.y) < Math.hypot(innerAt[best].x - p.x, innerAt[best].y - p.y) ? i : best), 0);
25
+ const step = (n) => (animate ? { animationDelay: `${n * 60}ms` } : undefined);
26
+ const enter = animate ? "motion-safe:animate-scale-in [animation-fill-mode:both]" : "";
27
+ const fade = animate ? "motion-safe:animate-fade-in [animation-fill-mode:both]" : "";
28
+ const lines = [
29
+ ...innerAt.map((p, i) => ({ from: { x: 50, y: 50 }, to: p, on: lit.has(inner[i].name), n: i })),
30
+ ...(innerAt.length ? outerAt.map((p, i) => ({ from: innerAt[nearest(p)], to: p, on: lit.has(outer[i].name), n: inner.length + i })) : []),
31
+ ];
32
+ return (_jsxs("div", { "data-slot": "network-graph", role: label ? "img" : undefined, "aria-label": label, "aria-hidden": label ? undefined : true,
33
+ // A set width (not w-full), so it keeps its size inside a parent that shrinks to fit; max-w-full lets it
34
+ // shrink on a narrow screen.
35
+ className: cn("relative aspect-square max-w-full shrink-0", s.width, className), children: [_jsx("svg", { viewBox: "0 0 100 100", className: "absolute inset-0 size-full", "aria-hidden": "true", children: lines.map((l, i) => (_jsx("line", { x1: l.from.x, y1: l.from.y, x2: l.to.x, y2: l.to.y, vectorEffect: "non-scaling-stroke", strokeWidth: l.on ? 1.5 : 1, strokeDasharray: l.on ? undefined : "3 3", className: cn(l.on ? "stroke-[var(--primary)]" : "stroke-[var(--text-disabled)]", fade), style: step(l.n) }, i))) }), _jsxs("div", { "aria-hidden": "true", className: "contents", children: [[...inner.map((p, i) => ({ p, at: innerAt[i], ring: "inner", n: i })), ...outer.map((p, i) => ({ p, at: outerAt[i], ring: "outer", n: inner.length + i }))].map(({ p, at, ring, n }) => (_jsx("span", { className: cn("absolute -translate-x-1/2 -translate-y-1/2", enter), style: { left: `${at.x}%`, top: `${at.y}%`, ...step(n + 1) }, children: _jsx(Avatar, { name: p.name, src: p.src, size: ring === "inner" ? s.inner : s.outer, className: cn(lit.has(p.name) && "ring-2 ring-[var(--primary)] ring-offset-2 ring-offset-[var(--surface-primary)]") }) }, `${p.name}-${n}`))), _jsx("span", { className: cn("absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2", enter), children: center ? (_jsx(Avatar, { name: center.name, src: center.src, size: s.center })) : (_jsx("span", { className: cn("flex items-center justify-center rounded-full bg-[var(--primary-solid)] font-bold text-white", s.you), children: "You" })) })] })] }));
36
+ }
37
+ export { NetworkGraph };
@@ -0,0 +1,20 @@
1
+ import type * as React from "react";
2
+ import { type VariantProps } from "class-variance-authority";
3
+ declare const pageBannerVariants: (props?: ({
4
+ variant?: "warning" | "error" | "success" | "info" | null | undefined;
5
+ placement?: "inline" | "page" | null | undefined;
6
+ } & import("class-variance-authority/types").ClassProp) | undefined) => string;
7
+ type PageBannerProps = VariantProps<typeof pageBannerVariants> & {
8
+ /** A bare Lucide icon; takes the banner's colour. */
9
+ icon?: React.ReactNode;
10
+ /** One short line. */
11
+ children: React.ReactNode;
12
+ /** One link or button at the end. */
13
+ action?: React.ReactNode;
14
+ /** With placement "page": the page column's width class (LAYOUT_FORM, LAYOUT_WIDE…) so the text lines up. */
15
+ contentWidthClass?: string;
16
+ className?: string;
17
+ };
18
+ declare function PageBanner({ variant, placement, icon, children, action, contentWidthClass, className }: PageBannerProps): React.JSX.Element;
19
+ export { PageBanner, pageBannerVariants };
20
+ export type { PageBannerProps };
@@ -0,0 +1,29 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { cva } from "class-variance-authority";
3
+ import { cn } from "@brdgapp/ui/lib/utils";
4
+ // The full-width alert strip under the top navigation. Ported from Bridge OS
5
+ // src/components/ui/page-banner.tsx with the same API. New: `placement="inline"` for a banner inside a
6
+ // card or a page that isn't in Bridge OS's shell. The default, "page", behaves exactly as before.
7
+ const pageBannerVariants = cva("border-b", {
8
+ variants: {
9
+ variant: {
10
+ // `info` has no -text token, so it uses --info for text and icon.
11
+ info: "border-[var(--info-border)] bg-[var(--info-bg)] text-[var(--info)]",
12
+ warning: "border-[var(--warning-border)] bg-[var(--warning-bg)] text-[var(--warning-text)]",
13
+ success: "border-[var(--success-border)] bg-[var(--success-bg)] text-[var(--success-text)]",
14
+ error: "border-[var(--error-border)] bg-[var(--error-bg)] text-[var(--error-text)]",
15
+ },
16
+ placement: {
17
+ // Breaks out of the centred page column to the screen edges, cancels the shell's top padding so
18
+ // it sits flush under the 56px nav, and stays pinned there while scrolling.
19
+ page: "sticky top-14 z-30 -mt-8 mb-6 mx-[calc(50%-50vw)] w-screen py-3",
20
+ inline: "rounded-xl border py-3",
21
+ },
22
+ },
23
+ defaultVariants: { variant: "info", placement: "page" },
24
+ });
25
+ function PageBanner({ variant, placement, icon, children, action, contentWidthClass, className }) {
26
+ const isPage = (placement ?? "page") === "page";
27
+ return (_jsx("div", { "data-slot": "page-banner", className: cn(pageBannerVariants({ variant, placement }), className), children: _jsxs("div", { className: cn("flex items-center gap-2 [&_svg]:size-4 [&_svg]:shrink-0", isPage ? cn(contentWidthClass, "mx-auto px-4 sm:px-6 lg:px-8") : "px-4"), children: [icon ? (_jsx("span", { "data-slot": "page-banner-icon", "aria-hidden": "true", className: "inline-flex", children: icon })) : null, _jsx("div", { className: "min-w-0 flex-1 text-[13px] font-semibold", children: children }), action] }) }));
28
+ }
29
+ export { PageBanner, pageBannerVariants };
@@ -0,0 +1,12 @@
1
+ import type { ReactNode } from "react";
2
+ type SceneProps = {
3
+ /** The real components, laid out as the picture. Nothing in them can be clicked or focused. */
4
+ children: ReactNode;
5
+ /** `grid`: the dot grid. `blue`: a blue wash, for the one hero picture of a promo. */
6
+ tone?: "grid" | "blue";
7
+ /** Size and layout only: `h-56`, `aspect-[4/3]`. */
8
+ className?: string;
9
+ };
10
+ /** A picture made of Bridge's own components. Decorative: hidden from screen readers and can't be clicked. */
11
+ declare function Scene({ children, tone, className }: SceneProps): import("react").JSX.Element;
12
+ export { Scene };
@@ -0,0 +1,7 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { cn } from "@brdgapp/ui/lib/utils";
3
+ /** A picture made of Bridge's own components. Decorative: hidden from screen readers and can't be clicked. */
4
+ function Scene({ children, tone = "grid", className }) {
5
+ return (_jsxs("div", { "data-slot": "scene", "aria-hidden": "true", inert: true, className: cn("relative flex items-center justify-center overflow-hidden rounded-xl select-none [&_*]:pointer-events-none", tone === "grid" ? "bg-[var(--info-bg)] bg-dot-grid" : "bg-[var(--primary-solid)]", className), children: [tone === "blue" ? _jsx("span", { className: "absolute -top-1/3 -right-1/4 size-2/3 rounded-full bg-white/15 blur-3xl" }) : null, _jsx("div", { className: "relative", children: children })] }));
6
+ }
7
+ export { Scene };
@@ -0,0 +1,8 @@
1
+ import type * as React from "react";
2
+ type SearchInputProps = Omit<React.ComponentProps<"input">, "type"> & {
3
+ /** Clears the field. The clear button shows only when there is a value. */
4
+ onClear?: () => void;
5
+ };
6
+ declare function SearchInput({ className, value, onClear, ...props }: SearchInputProps): React.JSX.Element;
7
+ export { SearchInput };
8
+ export type { SearchInputProps };
@@ -0,0 +1,14 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { Input as InputPrimitive } from "@base-ui/react/input";
4
+ import { Search, X } from "lucide-react";
5
+ import { IconButton } from "@brdgapp/ui/components/icon-button";
6
+ import { cn } from "@brdgapp/ui/lib/utils";
7
+ // `className` goes on the wrapper, not the input, so a width limit keeps the clear button inside the pill.
8
+ function SearchInput({ className, value, onClear, ...props }) {
9
+ const hasValue = value !== undefined && String(value).length > 0;
10
+ return (_jsxs("div", { "data-slot": "search-input", className: cn("group relative", className), children: [_jsx(Search, { "aria-hidden": "true", className: "pointer-events-none absolute top-1/2 left-3 size-4 -translate-y-1/2 text-[var(--text-disabled)] transition-colors group-focus-within:text-[var(--primary)]" }), _jsx(InputPrimitive, { "data-slot": "search-input-field", type: "search", value: value, className: cn("h-11 md:h-10 w-full rounded-full border border-[var(--border-default)] bg-[var(--surface-secondary)] py-2 pr-10 pl-10", "text-16 md:text-14 text-[var(--text-primary)] placeholder:text-[var(--text-muted)]", "transition-all duration-150 ease-out outline-none", "focus:border-[var(--primary)] focus:bg-[var(--surface-primary)] focus:ring-2 focus:ring-[var(--primary)]/20",
11
+ // Hide the browser's own clear button; this component draws its own.
12
+ "[&::-webkit-search-cancel-button]:appearance-none"), ...props }), hasValue && onClear ? (_jsx(IconButton, { "aria-label": "Clear search", size: "sm", onClick: onClear, className: "absolute top-1/2 right-1.5 -translate-y-1/2", children: _jsx(X, {}) })) : null] }));
13
+ }
14
+ export { SearchInput };
@@ -0,0 +1,18 @@
1
+ import type * as React from "react";
2
+ type SegmentedOption<T extends string> = {
3
+ value: T;
4
+ label: React.ReactNode;
5
+ /** Required when `label` is an icon: the option's name for screen readers. */
6
+ ariaLabel?: string;
7
+ };
8
+ type SegmentedControlProps<T extends string> = {
9
+ options: SegmentedOption<T>[];
10
+ value: T;
11
+ onChange: (value: T) => void;
12
+ /** Names the whole control for screen readers: "Time range", "Appearance". */
13
+ "aria-label": string;
14
+ className?: string;
15
+ };
16
+ declare function SegmentedControl<T extends string>({ options, value, onChange, className, ...props }: SegmentedControlProps<T>): React.JSX.Element;
17
+ export { SegmentedControl };
18
+ export type { SegmentedControlProps, SegmentedOption };