@brdgapp/ui 0.1.1 → 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.
- package/CHANGELOG.md +83 -0
- package/README.md +39 -85
- package/components/action-menu.d.ts +65 -0
- package/components/action-menu.js +48 -0
- package/components/avatar.d.ts +50 -0
- package/components/avatar.js +91 -0
- package/components/badge.d.ts +12 -0
- package/components/badge.js +23 -0
- package/components/bottom-sheet.d.ts +17 -0
- package/components/bottom-sheet.js +12 -0
- package/components/brand-mark.d.ts +20 -0
- package/components/brand-mark.js +44 -0
- package/components/button-group.d.ts +25 -0
- package/components/button-group.js +53 -0
- package/components/button.d.ts +33 -0
- package/components/button.js +82 -0
- package/components/card.d.ts +78 -0
- package/components/card.js +75 -0
- package/components/checkbox.d.ts +22 -0
- package/components/checkbox.js +35 -0
- package/components/collapsible.d.ts +29 -0
- package/components/collapsible.js +38 -0
- package/components/column-filter-menu.d.ts +15 -0
- package/components/column-filter-menu.js +30 -0
- package/components/column-sort-button.d.ts +18 -0
- package/components/column-sort-button.js +14 -0
- package/components/confirm-dialog.d.ts +20 -0
- package/components/confirm-dialog.js +17 -0
- package/components/copy-field.d.ts +17 -0
- package/components/copy-field.js +36 -0
- package/components/data-state.d.ts +44 -0
- package/components/data-state.js +25 -0
- package/components/data-table.d.ts +65 -0
- package/components/data-table.js +84 -0
- package/components/dialog.d.ts +19 -0
- package/components/dialog.js +10 -0
- package/components/drawer.d.ts +21 -0
- package/components/drawer.js +17 -0
- package/components/expandable-bio.d.ts +8 -0
- package/components/expandable-bio.js +26 -0
- package/components/field.d.ts +29 -0
- package/components/field.js +27 -0
- package/components/filter-chip.d.ts +11 -0
- package/components/filter-chip.js +8 -0
- package/components/filter-pill.d.ts +34 -0
- package/components/filter-pill.js +35 -0
- package/components/hover-card.d.ts +21 -0
- package/components/hover-card.js +20 -0
- package/components/icon-button.d.ts +16 -0
- package/components/icon-button.js +32 -0
- package/components/illustration.d.ts +33 -0
- package/components/illustration.js +70 -0
- package/components/info-hint.d.ts +29 -0
- package/components/info-hint.js +42 -0
- package/components/input.d.ts +9 -0
- package/components/input.js +15 -0
- package/components/network-graph.d.ts +48 -0
- package/components/network-graph.js +37 -0
- package/components/page-banner.d.ts +20 -0
- package/components/page-banner.js +29 -0
- package/components/scene.d.ts +12 -0
- package/components/scene.js +7 -0
- package/components/search-input.d.ts +8 -0
- package/components/search-input.js +14 -0
- package/components/segmented-control.d.ts +18 -0
- package/components/segmented-control.js +14 -0
- package/components/skeleton.d.ts +12 -0
- package/components/skeleton.js +21 -0
- package/components/skip-link.d.ts +8 -0
- package/components/skip-link.js +7 -0
- package/components/stat-box.d.ts +23 -0
- package/components/stat-box.js +29 -0
- package/components/switch.d.ts +13 -0
- package/components/switch.js +8 -0
- package/components/tabs.d.ts +6 -0
- package/components/tabs.js +25 -0
- package/components/tag-input.d.ts +15 -0
- package/components/tag-input.js +40 -0
- package/components/tag.d.ts +27 -0
- package/components/tag.js +35 -0
- package/components/textarea.d.ts +12 -0
- package/components/textarea.js +78 -0
- package/components/theme-switch.d.ts +6 -0
- package/components/theme-switch.js +16 -0
- package/components/toast.d.ts +25 -0
- package/components/toast.js +44 -0
- package/hooks/use-is-mobile.d.ts +2 -0
- package/hooks/use-is-mobile.js +16 -0
- package/hooks/use-mock-query.d.ts +23 -0
- package/hooks/use-mock-query.js +36 -0
- package/hooks/use-theme.d.ts +11 -0
- package/hooks/use-theme.js +34 -0
- package/lib/demo-controls.d.ts +24 -0
- package/lib/demo-controls.js +24 -0
- package/lib/format.d.ts +17 -0
- package/lib/format.js +55 -0
- package/lib/layout.d.ts +19 -0
- package/lib/layout.js +24 -0
- package/lib/motion.d.ts +16 -0
- package/lib/motion.js +18 -0
- package/lib/theme.d.ts +21 -0
- package/lib/theme.js +63 -0
- package/lib/utils.d.ts +22 -0
- package/lib/utils.js +27 -0
- package/package.json +32 -16
- package/styles/globals.css +595 -0
- package/src/index.ts +0 -18
- package/src/network-people-helper/README.md +0 -130
- package/src/network-people-helper/api.ts +0 -174
- package/src/network-people-helper/contact-avatar.tsx +0 -63
- package/src/network-people-helper/domain.ts +0 -14
- package/src/network-people-helper/icons.tsx +0 -130
- package/src/network-people-helper/index.ts +0 -6
- package/src/network-people-helper/network-people-helper.tsx +0 -397
- package/src/network-people-helper/network-people-nudge.tsx +0 -98
- package/src/network-people-helper/open-helper.ts +0 -22
- package/src/network-people-helper/person-menu.tsx +0 -211
- package/src/network-people-helper/styles.css +0 -848
- package/src/network-people-helper/types.ts +0 -23
- package/src/network-people-helper/use-network-people-list.ts +0 -122
- package/src/network-people-helper/use-network-preview.ts +0 -43
- package/src/provider.tsx +0 -59
- package/src/styles.css +0 -1
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { cn } from "@brdgapp/ui/lib/utils";
|
|
3
|
+
/** Brands whose own rules allow a one-color version. Google, Microsoft and Chrome are always in color. */
|
|
4
|
+
const CAN_BE_MONO = new Set(["linkedin", "whatsapp", "telegram", "apple"]);
|
|
5
|
+
const NAME = {
|
|
6
|
+
linkedin: "LinkedIn",
|
|
7
|
+
google: "Google",
|
|
8
|
+
microsoft: "Microsoft",
|
|
9
|
+
apple: "Apple",
|
|
10
|
+
whatsapp: "WhatsApp",
|
|
11
|
+
telegram: "Telegram",
|
|
12
|
+
chrome: "Chrome",
|
|
13
|
+
};
|
|
14
|
+
const LINKEDIN = "M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z";
|
|
15
|
+
const APPLE = "M16.365 1.43c0 1.14-.415 2.205-1.19 3.03-.81.87-2.145 1.54-3.28 1.45-.145-1.09.42-2.24 1.17-3.02.81-.85 2.205-1.47 3.3-1.46zM20.7 17.39c-.52 1.2-.77 1.73-1.44 2.79-.94 1.45-2.26 3.26-3.9 3.28-1.46.02-1.84-.95-3.82-.94-1.99.01-2.4.96-3.86.94-1.64-.02-2.89-1.65-3.83-3.1C1.88 17.42.5 12.84 2.4 9.64c.95-1.6 2.45-2.61 4.14-2.61 1.54 0 2.51.99 3.79.99 1.24 0 2-.99 3.8-.99 1.52 0 3.13.83 4.07 2.26-3.58 1.97-3 7.1.5 8.1z";
|
|
16
|
+
const WHATSAPP_RING = "M360,0C161.18,0,0,161.18,0,360c0,65.41,17.45,126.75,47.94,179.61L0,720l187.02-44.21c51.34,28.18,110.28,44.21,172.98,44.21,198.82,0,360-161.18,360-360S558.82,0,360,0ZM360,655.52c-60.17,0-116.13-17.98-162.82-48.87l-110.49,28.14,30.99-105.61c-33.53-47.93-53.2-106.26-53.2-169.19,0-163.21,132.31-295.52,295.52-295.52s295.52,132.31,295.52,295.52-132.31,295.52-295.52,295.52Z";
|
|
17
|
+
const WHATSAPP_PHONE = "M444.35,407.52l87.1,41.06c4,1.88,6.56,5.94,6.2,10.34-.94,11.46-5.54,34.43-26.13,55.02-58.12,58.12-162.49-7.64-166.74-10.18-25.67-13.79-50.06-32.24-73.19-55.36-23.12-23.12-41.58-47.52-55.37-73.19-2.55-4.24-68.31-108.61-10.18-166.74,20.59-20.59,43.56-25.19,55.02-26.13,4.41-.36,8.46,2.2,10.34,6.2l41.07,87.1c1.94,4.12,1.09,9.02-2.13,12.24l-30.61,30.61c-6.62,6.62-8.56,16.93-4,25.11,11.17,20.03,26.19,39.32,43.59,57.07,17.75,17.4,37.04,32.43,57.07,43.59,8.18,4.56,18.48,2.62,25.11-4l30.61-30.61c3.22-3.22,8.12-4.08,12.24-2.13Z";
|
|
18
|
+
const TELEGRAM_PLANE = "M5.4 11.7 16.9 7.3c.5-.2 1 .1.8.9l-2 9.3c-.1.6-.5.8-1 .5l-2.9-2.1-1.4 1.3c-.2.2-.3.3-.6.3l.2-3 5.4-4.9c.2-.2 0-.3-.3-.1l-6.7 4.2-2.9-.9c-.6-.2-.6-.6.1-.9Z";
|
|
19
|
+
const CIRCLE_24 = "M12 0a12 12 0 1 0 0 24 12 12 0 0 0 0-24Z";
|
|
20
|
+
function Drawing({ brand, mono }) {
|
|
21
|
+
switch (brand) {
|
|
22
|
+
case "linkedin":
|
|
23
|
+
return (_jsxs("svg", { viewBox: "0 0 24 24", children: [mono ? null : _jsx("rect", { x: "2", y: "2", width: "20", height: "20", fill: "#fff" }), _jsx("path", { d: LINKEDIN, fill: mono ? "currentColor" : "#0A66C2" })] }));
|
|
24
|
+
case "google":
|
|
25
|
+
return (_jsxs("svg", { viewBox: "0 0 48 48", children: [_jsx("path", { fill: "#FFC107", d: "M43.611 20.083H42V20H24v8h11.303c-1.649 4.657-6.08 8-11.303 8-6.627 0-12-5.373-12-12s5.373-12 12-12c3.059 0 5.842 1.154 7.961 3.039l5.657-5.657C34.046 6.053 29.268 4 24 4 12.955 4 4 12.955 4 24s8.955 20 20 20 20-8.955 20-20c0-1.341-.138-2.65-.389-3.917z" }), _jsx("path", { fill: "#FF3D00", d: "M6.306 14.691l6.571 4.819C14.655 15.108 18.961 12 24 12c3.059 0 5.842 1.154 7.961 3.039l5.657-5.657C34.046 6.053 29.268 4 24 4 16.318 4 9.656 8.337 6.306 14.691z" }), _jsx("path", { fill: "#4CAF50", d: "M24 44c5.166 0 9.86-1.977 13.409-5.192l-6.19-5.238C29.211 35.091 26.715 36 24 36c-5.202 0-9.619-3.317-11.283-7.946l-6.522 5.025C9.505 39.556 16.227 44 24 44z" }), _jsx("path", { fill: "#1976D2", d: "M43.611 20.083H42V20H24v8h11.303c-.792 2.237-2.231 4.166-4.087 5.571.001-.001.002-.001.003-.002l6.19 5.238C36.971 39.205 44 34 44 24c0-1.341-.138-2.65-.389-3.917z" })] }));
|
|
26
|
+
case "microsoft":
|
|
27
|
+
return (_jsxs("svg", { viewBox: "0 0 23 23", children: [_jsx("path", { d: "M1 1h10v10H1V1z", fill: "#F25022" }), _jsx("path", { d: "M12 1h10v10H12V1z", fill: "#7FBA00" }), _jsx("path", { d: "M1 12h10v10H1V12z", fill: "#00A4EF" }), _jsx("path", { d: "M12 12h10v10H12V12z", fill: "#FFB900" })] }));
|
|
28
|
+
case "apple":
|
|
29
|
+
// Apple's mark is one color: it follows the text, so it's black on light and white on dark.
|
|
30
|
+
return (_jsx("svg", { viewBox: "0 0 24 24", children: _jsx("path", { d: APPLE, fill: "currentColor" }) }));
|
|
31
|
+
case "whatsapp":
|
|
32
|
+
return (_jsxs("svg", { viewBox: "0 0 720 720", fill: mono ? "currentColor" : "#25D366", children: [_jsx("path", { d: WHATSAPP_RING }), _jsx("path", { d: WHATSAPP_PHONE })] }));
|
|
33
|
+
case "telegram":
|
|
34
|
+
return mono ? (_jsx("svg", { viewBox: "0 0 24 24", children: _jsx("path", { d: `${CIRCLE_24}${TELEGRAM_PLANE}`, fill: "currentColor", fillRule: "evenodd" }) })) : (_jsxs("svg", { viewBox: "0 0 24 24", children: [_jsx("circle", { cx: "12", cy: "12", r: "12", fill: "#2AABEE" }), _jsx("path", { d: TELEGRAM_PLANE, fill: "#fff" })] }));
|
|
35
|
+
case "chrome":
|
|
36
|
+
return (_jsxs("svg", { viewBox: "2 2 44 44", children: [_jsx("path", { fill: "#EA4335", d: "M15.34 29 5.542 12.029A22 22 0 0 1 43.596 14H24a10 10 0 0 0-8.66 15Z" }), _jsx("path", { fill: "#FBBC04", d: "M24 14h19.596a22 22 0 0 1-20.734 31.971L32.66 29A10 10 0 0 0 24 14Z" }), _jsx("path", { fill: "#34A853", d: "M32.66 29 22.862 45.971A22 22 0 0 1 5.542 12.029L15.34 29a10 10 0 0 0 17.32 0Z" }), _jsx("circle", { cx: "24", cy: "24", r: "10", fill: "#fff" }), _jsx("circle", { cx: "24", cy: "24", r: "8", fill: "#1A73E8" })] }));
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/** A company's logo at icon size, in its own colors. Not an icon: Lucide has none of these. */
|
|
40
|
+
function BrandMark({ brand, label = false, mono = false, className }) {
|
|
41
|
+
const oneColor = mono && CAN_BE_MONO.has(brand);
|
|
42
|
+
return (_jsx("span", { "data-slot": "brand-mark", role: label ? "img" : undefined, "aria-label": label ? NAME[brand] : undefined, "aria-hidden": label ? undefined : true, className: cn("inline-flex size-4 shrink-0 [&>svg]:size-full", className), children: _jsx(Drawing, { brand: brand, mono: oneColor }) }));
|
|
43
|
+
}
|
|
44
|
+
export { BrandMark, NAME as BRAND_NAMES };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type * as React from "react";
|
|
2
|
+
import { Button as ButtonPrimitive } from "@base-ui/react/button";
|
|
3
|
+
type ButtonGroupVariant = "secondary" | "primary";
|
|
4
|
+
declare const buttonGroupVariants: (props?: ({
|
|
5
|
+
variant?: "secondary" | "primary" | null | undefined;
|
|
6
|
+
} & import("class-variance-authority/types").ClassProp) | undefined) => string;
|
|
7
|
+
declare function ButtonGroup({ variant, className, children, ...props }: {
|
|
8
|
+
variant?: ButtonGroupVariant;
|
|
9
|
+
/** Name the group for screen readers when the actions need context: "Intro actions". */
|
|
10
|
+
"aria-label"?: string;
|
|
11
|
+
className?: string;
|
|
12
|
+
children: React.ReactNode;
|
|
13
|
+
}): React.JSX.Element;
|
|
14
|
+
declare const buttonGroupItemVariants: (props?: ({
|
|
15
|
+
variant?: "secondary" | "primary" | null | undefined;
|
|
16
|
+
iconOnly?: boolean | null | undefined;
|
|
17
|
+
} & import("class-variance-authority/types").ClassProp) | undefined) => string;
|
|
18
|
+
type ButtonGroupItemProps = Omit<ButtonPrimitive.Props, "className"> & {
|
|
19
|
+
/** Tighter padding for a segment that is only an icon. It then needs an `aria-label`. */
|
|
20
|
+
iconOnly?: boolean;
|
|
21
|
+
className?: string;
|
|
22
|
+
};
|
|
23
|
+
declare function ButtonGroupItem({ iconOnly, className, render, children, ...props }: ButtonGroupItemProps): React.JSX.Element;
|
|
24
|
+
export { ButtonGroup, ButtonGroupItem, buttonGroupVariants, buttonGroupItemVariants };
|
|
25
|
+
export type { ButtonGroupVariant, ButtonGroupItemProps };
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { createContext, useContext } from "react";
|
|
4
|
+
import { Button as ButtonPrimitive } from "@base-ui/react/button";
|
|
5
|
+
import { mergeProps } from "@base-ui/react/merge-props";
|
|
6
|
+
import { useRender } from "@base-ui/react/use-render";
|
|
7
|
+
import { cva } from "class-variance-authority";
|
|
8
|
+
import { cn } from "@brdgapp/ui/lib/utils";
|
|
9
|
+
const ButtonGroupContext = createContext("secondary");
|
|
10
|
+
const buttonGroupVariants = cva(
|
|
11
|
+
// Ends are rounded on the children, not by clipping, so a segment's tooltip is never cut off.
|
|
12
|
+
"inline-flex items-stretch rounded-full [&>*+*]:border-l [&>*:first-child]:rounded-l-full [&>*:last-child]:rounded-r-full", {
|
|
13
|
+
variants: {
|
|
14
|
+
variant: {
|
|
15
|
+
secondary: "border border-[var(--text-disabled)] bg-[var(--surface-elevated)] [&>*+*]:border-[var(--border-divider)]",
|
|
16
|
+
primary: "border border-[var(--primary-solid)] bg-[var(--primary-solid)] [&>*+*]:border-white/25",
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
defaultVariants: { variant: "secondary" },
|
|
20
|
+
});
|
|
21
|
+
function ButtonGroup({ variant = "secondary", className, children, ...props }) {
|
|
22
|
+
return (_jsx(ButtonGroupContext.Provider, { value: variant, children: _jsx("div", { "data-slot": "button-group", role: "group", className: cn(buttonGroupVariants({ variant }), className), ...props, children: children }) }));
|
|
23
|
+
}
|
|
24
|
+
const buttonGroupItemVariants = cva([
|
|
25
|
+
"inline-flex h-8 items-center justify-center gap-1.5 text-[14px] font-semibold tracking-[0.4px]",
|
|
26
|
+
"transition-colors duration-150 outline-none focus-visible:ring-2 focus-visible:ring-inset",
|
|
27
|
+
"disabled:pointer-events-none disabled:opacity-50 [&_svg]:size-[15px] [&_svg]:shrink-0",
|
|
28
|
+
], {
|
|
29
|
+
variants: {
|
|
30
|
+
variant: {
|
|
31
|
+
secondary: "text-[var(--text-secondary)] hover:bg-[var(--surface-secondary)] hover:text-[var(--text-primary)] focus-visible:ring-[var(--primary)]",
|
|
32
|
+
primary: "text-white hover:bg-[var(--primary-solid-hover)] active:bg-[var(--primary-active)] focus-visible:ring-white",
|
|
33
|
+
},
|
|
34
|
+
iconOnly: { true: "px-2.5", false: "px-3.5" },
|
|
35
|
+
},
|
|
36
|
+
// Only the secondary icon-only segment mutes its glyph; on primary it stays white.
|
|
37
|
+
compoundVariants: [{ variant: "secondary", iconOnly: true, className: "text-[var(--text-muted)]" }],
|
|
38
|
+
defaultVariants: { variant: "secondary", iconOnly: false },
|
|
39
|
+
});
|
|
40
|
+
function ButtonGroupItem({ iconOnly = false, className, render, children, ...props }) {
|
|
41
|
+
const variant = useContext(ButtonGroupContext);
|
|
42
|
+
const classes = cn(buttonGroupItemVariants({ variant, iconOnly }), className);
|
|
43
|
+
// With `render` (a Next Link), the segment keeps link semantics.
|
|
44
|
+
const rendered = useRender({
|
|
45
|
+
render: render,
|
|
46
|
+
enabled: render !== undefined,
|
|
47
|
+
props: mergeProps({ className: classes, "data-slot": "button-group-item" }, props, { children }),
|
|
48
|
+
});
|
|
49
|
+
if (rendered)
|
|
50
|
+
return rendered;
|
|
51
|
+
return (_jsx(ButtonPrimitive, { "data-slot": "button-group-item", className: classes, ...props, children: children }));
|
|
52
|
+
}
|
|
53
|
+
export { ButtonGroup, ButtonGroupItem, buttonGroupVariants, buttonGroupItemVariants };
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type * as React from "react";
|
|
2
|
+
import { Button as ButtonPrimitive } from "@base-ui/react/button";
|
|
3
|
+
import { type VariantProps } from "class-variance-authority";
|
|
4
|
+
declare const buttonVariants: (props?: ({
|
|
5
|
+
variant?: "link" | "secondary" | "primary" | "ghost" | "destructive" | "danger-ghost" | null | undefined;
|
|
6
|
+
size?: "sm" | "md" | "lg" | "xs" | null | undefined;
|
|
7
|
+
fullWidth?: boolean | null | undefined;
|
|
8
|
+
} & import("class-variance-authority/types").ClassProp) | undefined) => string;
|
|
9
|
+
type ButtonProps = Omit<ButtonPrimitive.Props, "className"> & VariantProps<typeof buttonVariants> & {
|
|
10
|
+
className?: string;
|
|
11
|
+
/** A leading icon. Pass a bare Lucide icon; it is sized for you and hidden from screen readers. */
|
|
12
|
+
icon?: React.ReactNode;
|
|
13
|
+
/** The action is running: shows a spinner, disables the button and sets `aria-busy`. */
|
|
14
|
+
loading?: boolean;
|
|
15
|
+
};
|
|
16
|
+
declare function Button({ className, variant, size, fullWidth, icon, loading, disabled, render, children, ...props }: ButtonProps): React.JSX.Element;
|
|
17
|
+
declare const buttonSkeletonHeights: {
|
|
18
|
+
readonly xs: "h-7";
|
|
19
|
+
readonly sm: "h-8";
|
|
20
|
+
readonly md: "h-11 md:h-10";
|
|
21
|
+
readonly lg: "h-12";
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Placeholder for a button whose data hasn't loaded yet. Same height as the real button, so swapping
|
|
25
|
+
* it in causes no layout shift. Not the same as `loading`, which is an action already running.
|
|
26
|
+
*/
|
|
27
|
+
declare function ButtonSkeleton({ size, width, className, }: {
|
|
28
|
+
size?: keyof typeof buttonSkeletonHeights;
|
|
29
|
+
width?: number | string;
|
|
30
|
+
className?: string;
|
|
31
|
+
}): React.JSX.Element;
|
|
32
|
+
export { Button, ButtonSkeleton, buttonVariants };
|
|
33
|
+
export type { ButtonProps };
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Button as ButtonPrimitive } from "@base-ui/react/button";
|
|
4
|
+
import { mergeProps } from "@base-ui/react/merge-props";
|
|
5
|
+
import { useRender } from "@base-ui/react/use-render";
|
|
6
|
+
import { cva } from "class-variance-authority";
|
|
7
|
+
import { Loader2 } from "lucide-react";
|
|
8
|
+
import { cn } from "@brdgapp/ui/lib/utils";
|
|
9
|
+
// Started from shadcn's Base UI button (style base-nova), then moved onto Bridge's tokens and API.
|
|
10
|
+
// Look and API match Bridge OS src/components/ui/button.tsx (components.md, 2026-10-29); `asChild`
|
|
11
|
+
// became `render`. Fills use the AA-verified --primary-solid / --danger-solid, never --primary / --error.
|
|
12
|
+
const buttonVariants = cva([
|
|
13
|
+
"group/button inline-flex items-center gap-1.5 whitespace-nowrap font-semibold tracking-[0.4px]",
|
|
14
|
+
"transition-all duration-150 ease-out outline-none select-none",
|
|
15
|
+
"disabled:pointer-events-none disabled:opacity-50",
|
|
16
|
+
"[&_svg]:pointer-events-none [&_svg]:shrink-0",
|
|
17
|
+
], {
|
|
18
|
+
variants: {
|
|
19
|
+
variant: {
|
|
20
|
+
primary: "bg-[var(--primary-solid)] text-white hover:bg-[var(--primary-solid-hover)] active:bg-[var(--primary-active)] focus-visible:ring-[var(--primary)]",
|
|
21
|
+
// Bordered variants sit on a solid raised fill, never transparent, so page glow never shows
|
|
22
|
+
// through. The label names the control, so the grey border is decorative.
|
|
23
|
+
secondary: "border border-[var(--text-disabled)] bg-[var(--surface-elevated)] text-[var(--text-primary)] hover:border-[var(--text-muted)] hover:bg-[var(--surface-secondary)] focus-visible:ring-[var(--primary)]",
|
|
24
|
+
ghost: "bg-transparent text-[var(--text-secondary)] hover:bg-[var(--surface-tertiary)] focus-visible:ring-[var(--primary)]",
|
|
25
|
+
destructive: "bg-[var(--danger-solid)] text-white hover:bg-[var(--danger-solid-hover)] focus-visible:ring-[var(--error)]",
|
|
26
|
+
"danger-ghost": "bg-transparent text-[var(--danger-text)] hover:bg-[var(--error-bg)] focus-visible:ring-[var(--error)]",
|
|
27
|
+
// Inline text action: no pill, no padding, primary colour, underline on hover.
|
|
28
|
+
link: "justify-start bg-transparent p-0 text-[var(--primary)] underline-offset-2 hover:text-[var(--primary-hover)] hover:underline focus-visible:underline disabled:no-underline",
|
|
29
|
+
},
|
|
30
|
+
// xs (28px) is the smallest allowed: it clears the 24px minimum target.
|
|
31
|
+
size: {
|
|
32
|
+
xs: "text-[12px] [&_svg:not([class*='size-'])]:size-3.5",
|
|
33
|
+
sm: "text-[14px] [&_svg:not([class*='size-'])]:size-4",
|
|
34
|
+
md: "text-[14px] [&_svg:not([class*='size-'])]:size-4",
|
|
35
|
+
lg: "text-[16px] [&_svg:not([class*='size-'])]:size-[18px]",
|
|
36
|
+
},
|
|
37
|
+
fullWidth: {
|
|
38
|
+
true: "w-full",
|
|
39
|
+
false: "",
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
compoundVariants: [
|
|
43
|
+
// Every variant except `link` is a pill with a fixed height and padding.
|
|
44
|
+
{
|
|
45
|
+
variant: ["primary", "secondary", "ghost", "destructive", "danger-ghost"],
|
|
46
|
+
className: "justify-center rounded-full active:scale-[0.98] focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--surface-primary)]",
|
|
47
|
+
},
|
|
48
|
+
{ variant: ["primary", "secondary", "ghost", "destructive", "danger-ghost"], size: "xs", className: "h-7 px-3" },
|
|
49
|
+
{ variant: ["primary", "secondary", "ghost", "destructive", "danger-ghost"], size: "sm", className: "h-8 px-[14px] py-1" },
|
|
50
|
+
{ variant: ["primary", "secondary", "ghost", "destructive", "danger-ghost"], size: "md", className: "h-11 px-5 md:h-10" },
|
|
51
|
+
{ variant: ["primary", "secondary", "ghost", "destructive", "danger-ghost"], size: "lg", className: "h-12 px-6 py-3" },
|
|
52
|
+
// A link reads as text, so it never stretches.
|
|
53
|
+
{ variant: "link", fullWidth: true, className: "w-auto" },
|
|
54
|
+
],
|
|
55
|
+
defaultVariants: {
|
|
56
|
+
variant: "primary",
|
|
57
|
+
size: "sm",
|
|
58
|
+
fullWidth: false,
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
function Button({ className, variant, size, fullWidth, icon, loading = false, disabled, render, children, ...props }) {
|
|
62
|
+
const classes = cn(buttonVariants({ variant, size, fullWidth }), className);
|
|
63
|
+
// With `render` (for example a Next `Link`), the element keeps its own semantics: a link stays a
|
|
64
|
+
// link. It owns its content, so `icon` and `loading` don't apply.
|
|
65
|
+
const rendered = useRender({
|
|
66
|
+
render: render,
|
|
67
|
+
enabled: render !== undefined,
|
|
68
|
+
props: mergeProps({ className: classes, "data-slot": "button" }, props, { children }),
|
|
69
|
+
});
|
|
70
|
+
if (rendered)
|
|
71
|
+
return rendered;
|
|
72
|
+
return (_jsxs(ButtonPrimitive, { "data-slot": "button", className: classes, disabled: disabled || loading, "aria-busy": loading || undefined, ...props, children: [loading ? (_jsx(Loader2, { "data-slot": "button-spinner", className: "animate-spin", "aria-hidden": "true" })) : icon ? (_jsx("span", { "data-slot": "button-icon", className: "inline-flex shrink-0", "aria-hidden": "true", children: icon })) : null, children] }));
|
|
73
|
+
}
|
|
74
|
+
const buttonSkeletonHeights = { xs: "h-7", sm: "h-8", md: "h-11 md:h-10", lg: "h-12" };
|
|
75
|
+
/**
|
|
76
|
+
* Placeholder for a button whose data hasn't loaded yet. Same height as the real button, so swapping
|
|
77
|
+
* it in causes no layout shift. Not the same as `loading`, which is an action already running.
|
|
78
|
+
*/
|
|
79
|
+
function ButtonSkeleton({ size = "sm", width = "7rem", className, }) {
|
|
80
|
+
return (_jsx("span", { "data-slot": "button-skeleton", "aria-hidden": "true", style: { width }, className: cn("inline-block animate-pulse rounded-full bg-[var(--border-hover)] motion-reduce:animate-none", buttonSkeletonHeights[size], className) }));
|
|
81
|
+
}
|
|
82
|
+
export { Button, ButtonSkeleton, buttonVariants };
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { type VariantProps } from "class-variance-authority";
|
|
3
|
+
declare const cardVariants: (props?: ({
|
|
4
|
+
size?: "sm" | "md" | "lg" | null | undefined;
|
|
5
|
+
} & import("class-variance-authority/types").ClassProp) | undefined) => string;
|
|
6
|
+
type CardProps = VariantProps<typeof cardVariants> & {
|
|
7
|
+
/** No padding: for a table or list that runs edge to edge. Pair with a header strip. */
|
|
8
|
+
flush?: boolean;
|
|
9
|
+
/** No glass and no side padding: a row inside another card or pane, split from its neighbours by dividers. */
|
|
10
|
+
bare?: boolean;
|
|
11
|
+
/** The whole card goes to this page. It rises on hover. */
|
|
12
|
+
href?: string;
|
|
13
|
+
/** Your router's link (Next `Link`) for `href`. A plain `<a>` by default. */
|
|
14
|
+
linkAs?: React.ElementType;
|
|
15
|
+
/** The whole card picks this item, usually to open it in a pane. It tints on hover. */
|
|
16
|
+
onSelect?: () => void;
|
|
17
|
+
/** The picked card: blue border and tint. Use with `onSelect`. */
|
|
18
|
+
selected?: boolean;
|
|
19
|
+
/** Can't be clicked; faded. */
|
|
20
|
+
disabled?: boolean;
|
|
21
|
+
/** What the card is called, when it has no CardHeader to take the name from. Clickable cards need a name. */
|
|
22
|
+
label?: string;
|
|
23
|
+
/** Layout only: `w-full`, `max-w-md`, `col-span-2`. */
|
|
24
|
+
className?: string;
|
|
25
|
+
children?: React.ReactNode;
|
|
26
|
+
};
|
|
27
|
+
declare function Card({ size, flush, bare, href, linkAs: LinkComponent, onSelect, selected, disabled, label, className, children, }: CardProps): React.JSX.Element;
|
|
28
|
+
declare const ICON_COLOR: {
|
|
29
|
+
readonly blue: "bg-[var(--primary)]/8 text-[var(--primary)]";
|
|
30
|
+
readonly green: "bg-[var(--success)]/8 text-[var(--success-text)]";
|
|
31
|
+
readonly amber: "bg-[var(--warning)]/10 text-[var(--warning-text)]";
|
|
32
|
+
readonly purple: "bg-[var(--purple)]/8 text-[var(--purple)]";
|
|
33
|
+
};
|
|
34
|
+
type CardHeaderProps = {
|
|
35
|
+
/** The card's name. 16px bold; 14px in a strip or a `sm` card. */
|
|
36
|
+
title: React.ReactNode;
|
|
37
|
+
/** A bare Lucide icon, set in a 36px blue square. */
|
|
38
|
+
icon?: React.ReactNode;
|
|
39
|
+
/** The icon square's color, by topic. */
|
|
40
|
+
iconColor?: keyof typeof ICON_COLOR;
|
|
41
|
+
/** A short explanation in an ⓘ beside the title: what a number counts, what a term means. */
|
|
42
|
+
hint?: string;
|
|
43
|
+
/** Anything else in front of the title: an Avatar, a company logo. Use instead of `icon`. */
|
|
44
|
+
leading?: React.ReactNode;
|
|
45
|
+
/** One muted line under the title. */
|
|
46
|
+
description?: React.ReactNode;
|
|
47
|
+
/** Small things at the top right, before the action: a date, a "New" Tag. */
|
|
48
|
+
meta?: React.ReactNode;
|
|
49
|
+
/** One control on the right: a link button, an IconButton or an ActionMenu. */
|
|
50
|
+
action?: React.ReactNode;
|
|
51
|
+
/** A tinted band across the top of the card, split from the body by a line. For dense cards and tables. */
|
|
52
|
+
strip?: boolean;
|
|
53
|
+
/** The title's heading level. Match the page: `h2` under the page title. */
|
|
54
|
+
as?: "h2" | "h3" | "h4";
|
|
55
|
+
/** 14px title, for `sm` cards and rows. */
|
|
56
|
+
compact?: boolean;
|
|
57
|
+
};
|
|
58
|
+
declare function CardHeader({ title, icon, iconColor, hint, leading, description, meta, action, strip, as: Heading, compact }: CardHeaderProps): React.JSX.Element;
|
|
59
|
+
type CardMediaProps = {
|
|
60
|
+
/** The picture. */
|
|
61
|
+
src: string;
|
|
62
|
+
/** What it shows, or "" when it's decoration. */
|
|
63
|
+
alt: string;
|
|
64
|
+
/** Height in pixels. */
|
|
65
|
+
height?: number;
|
|
66
|
+
};
|
|
67
|
+
/** A picture across the top of the card, edge to edge. Put it first. */
|
|
68
|
+
declare function CardMedia({ src, alt, height }: CardMediaProps): React.JSX.Element;
|
|
69
|
+
type CardFooterProps = {
|
|
70
|
+
/** Buttons, lined up on the right, main one last. */
|
|
71
|
+
children: React.ReactNode;
|
|
72
|
+
/** Layout only. */
|
|
73
|
+
className?: string;
|
|
74
|
+
};
|
|
75
|
+
/** A row of buttons under a line, at the bottom of the card. */
|
|
76
|
+
declare function CardFooter({ children, className }: CardFooterProps): React.JSX.Element;
|
|
77
|
+
export { Card, CardFooter, CardHeader, CardMedia, cardVariants };
|
|
78
|
+
export type { CardFooterProps, CardHeaderProps, CardMediaProps, CardProps };
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { cva } from "class-variance-authority";
|
|
5
|
+
import { InfoHint } from "@brdgapp/ui/components/info-hint";
|
|
6
|
+
import { cn } from "@brdgapp/ui/lib/utils";
|
|
7
|
+
// A frosted-glass card: the box most of Bridge sits in. Started from shadcn's card, then rebuilt on
|
|
8
|
+
// Bridge's glass after a survey of all 12 app repos (2026-11-03): over 1,500 hand-made or shadcn cards.
|
|
9
|
+
//
|
|
10
|
+
// Decisions:
|
|
11
|
+
// - Glass only (`liquid-glass`), picked by Aman. Corners and padding by size: `lg` 16px / 24px for big
|
|
12
|
+
// cards, `md` 12px / 20px for tiles, `sm` 12px / 16px for list items (app-cc's inbox, raise's pipeline).
|
|
13
|
+
// - `bare`: the same content with no glass, for a card that sits in another card or pane as a
|
|
14
|
+
// divider-separated row. From Bridge OS activity-feed-item.tsx (`bare`) and app-cc's compact list.
|
|
15
|
+
// - Clickable cards, two jobs: `href` goes to a page (perks, directories, bridge-jobs); `onSelect` picks
|
|
16
|
+
// the card, usually to open it in a pane (app-cc inbox, app-intercom, raise pipeline, Bridge OS feed).
|
|
17
|
+
// Built the way app-portfolio does it: an invisible link or button covers the card and gets the click
|
|
18
|
+
// and keyboard focus; the content lets clicks through, except its own links, buttons and fields, which
|
|
19
|
+
// work as usual. So no screen writes stopPropagation (the apps do, over 300 times).
|
|
20
|
+
// - Hover by job (Aman): links rise 2px with a soft shadow (Elevation); selectable cards tint, since a
|
|
21
|
+
// list of rising cards jumps about.
|
|
22
|
+
// - `selected`: a blue border and a light blue tint, what app-cc, app-intercom and app-portfolio already
|
|
23
|
+
// use; announced with aria-current.
|
|
24
|
+
// - No colored edge bars or strips (Aman): status goes in a Tag in the header, word and color together.
|
|
25
|
+
// - The glass and states live in globals.css (`[data-slot=card]` rules), because the glass rule outranks
|
|
26
|
+
// Tailwind's background, border and shadow classes.
|
|
27
|
+
const cardVariants = cva("relative block text-[var(--text-body)]", {
|
|
28
|
+
variants: {
|
|
29
|
+
size: {
|
|
30
|
+
lg: "rounded-2xl [--card-pad:24px]",
|
|
31
|
+
md: "rounded-xl [--card-pad:20px]",
|
|
32
|
+
sm: "rounded-xl [--card-pad:16px]",
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
defaultVariants: { size: "lg" },
|
|
36
|
+
});
|
|
37
|
+
const CardContext = React.createContext(null);
|
|
38
|
+
function Card({ size, flush = false, bare = false, href, linkAs: LinkComponent = "a", onSelect, selected = false, disabled = false, label, className, children, }) {
|
|
39
|
+
const titleId = React.useId();
|
|
40
|
+
const clickable = href ? "link" : onSelect ? "select" : undefined;
|
|
41
|
+
const nameProps = label ? { "aria-label": label } : { "aria-labelledby": titleId };
|
|
42
|
+
let cover = null;
|
|
43
|
+
if (clickable === "link" && !disabled) {
|
|
44
|
+
cover = _jsx(LinkComponent, { "data-card-cover": "", href: href, "aria-current": selected ? "page" : undefined, ...nameProps });
|
|
45
|
+
}
|
|
46
|
+
else if (clickable) {
|
|
47
|
+
cover = (_jsx("button", { type: "button", "data-card-cover": "", disabled: disabled, onClick: onSelect, "aria-current": selected ? "true" : undefined, ...nameProps }));
|
|
48
|
+
}
|
|
49
|
+
return (_jsx(CardContext.Provider, { value: { titleId }, children: _jsxs("section", { "data-slot": "card", "data-clickable": clickable, "data-selected": selected ? "" : undefined, "data-disabled": disabled ? "" : undefined, "data-bare": bare ? "" : undefined, className: cn(cardVariants({ size }), bare ? "-mx-3 rounded-lg px-3 py-3.5 [--card-inset:0px]" : "liquid-glass", !bare && (flush ? "[--card-inset:0px]" : "p-[var(--card-pad)] [--card-inset:var(--card-pad)]"), clickable === "link" && !disabled && "transition-[translate,box-shadow] duration-150 ease-out hover:-translate-y-0.5 motion-reduce:transition-none motion-reduce:hover:translate-y-0", clickable && !disabled && "cursor-pointer", disabled && "opacity-60", className), children: [cover, children] }) }));
|
|
50
|
+
}
|
|
51
|
+
// The icon square's color. Blue by default; Bridge OS also tints squares green, amber and purple by topic
|
|
52
|
+
// (outcome value in green, alumni in honey…). The same names as Tag's colors.
|
|
53
|
+
const ICON_COLOR = {
|
|
54
|
+
blue: "bg-[var(--primary)]/8 text-[var(--primary)]",
|
|
55
|
+
green: "bg-[var(--success)]/8 text-[var(--success-text)]",
|
|
56
|
+
amber: "bg-[var(--warning)]/10 text-[var(--warning-text)]",
|
|
57
|
+
purple: "bg-[var(--purple)]/8 text-[var(--purple)]",
|
|
58
|
+
};
|
|
59
|
+
function CardHeader({ title, icon, iconColor = "blue", hint, leading, description, meta, action, strip = false, as: Heading = "h2", compact = false }) {
|
|
60
|
+
const ctx = React.useContext(CardContext);
|
|
61
|
+
return (_jsxs("header", { "data-slot": "card-header", className: cn("flex gap-3", description ? "items-start" : "items-center", strip
|
|
62
|
+
? "-mx-[var(--card-inset)] -mt-[var(--card-inset)] mb-[var(--card-inset)] items-center border-b border-[var(--border-divider)] bg-[var(--surface-secondary)]/50 px-[var(--card-pad)] py-3.5"
|
|
63
|
+
: "mb-4 last:mb-0"), children: [icon ? (_jsx("span", { "aria-hidden": "true", className: cn("flex size-9 shrink-0 items-center justify-center rounded-lg [&_svg]:size-[18px]", ICON_COLOR[iconColor]), children: icon })) : leading ? (_jsx("span", { className: "flex shrink-0", children: leading })) : null, _jsxs("div", { className: "min-w-0 flex-1", children: [_jsxs("div", { className: "flex min-w-0 items-center gap-1.5", children: [_jsx(Heading, { id: ctx?.titleId, "data-slot": "card-title", className: cn("truncate font-bold tracking-tight text-[var(--text-primary)]", strip || compact ? "text-14" : "text-16"), children: title }), hint ? _jsx(InfoHint, { text: hint }) : null] }), description ? (_jsx("p", { "data-slot": "card-description", className: "mt-0.5 text-[13px] text-[var(--text-muted)]", children: description })) : null] }), meta ? (_jsx("div", { "data-slot": "card-meta", className: "flex shrink-0 items-center gap-2 text-[12px] tabular-nums text-[var(--text-muted)]", children: meta })) : null, action ? (_jsx("div", { "data-slot": "card-action", className: "shrink-0", children: action })) : null] }));
|
|
64
|
+
}
|
|
65
|
+
/** A picture across the top of the card, edge to edge. Put it first. */
|
|
66
|
+
function CardMedia({ src, alt, height = 128 }) {
|
|
67
|
+
return (
|
|
68
|
+
// eslint-disable-next-line @next/next/no-img-element -- package component; apps pass any image URL
|
|
69
|
+
_jsx("img", { "data-slot": "card-media", src: src, alt: alt, style: { height }, className: "-mx-[var(--card-inset)] -mt-[var(--card-inset)] mb-[var(--card-pad)] block w-[calc(100%+2*var(--card-inset))] max-w-none object-cover" }));
|
|
70
|
+
}
|
|
71
|
+
/** A row of buttons under a line, at the bottom of the card. */
|
|
72
|
+
function CardFooter({ children, className }) {
|
|
73
|
+
return (_jsx("footer", { "data-slot": "card-footer", className: cn("mt-5 flex items-center justify-end gap-2 border-t border-[var(--border-divider)] pt-4", className), children: children }));
|
|
74
|
+
}
|
|
75
|
+
export { Card, CardFooter, CardHeader, CardMedia, cardVariants };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { Checkbox as CheckboxPrimitive } from "@base-ui/react/checkbox";
|
|
3
|
+
import { CheckboxGroup as CheckboxGroupPrimitive } from "@base-ui/react/checkbox-group";
|
|
4
|
+
type CheckboxProps = Omit<CheckboxPrimitive.Root.Props, "className" | "render"> & {
|
|
5
|
+
/** Text beside the box. Clicking it ticks the box. Without it, give the box an `aria-label`. */
|
|
6
|
+
label?: React.ReactNode;
|
|
7
|
+
/** A line of help under the label. */
|
|
8
|
+
description?: React.ReactNode;
|
|
9
|
+
/** Layout only: `mt-1`. */
|
|
10
|
+
className?: string;
|
|
11
|
+
};
|
|
12
|
+
declare function Checkbox({ label, description, className, disabled, ...props }: CheckboxProps): React.JSX.Element;
|
|
13
|
+
type CheckboxGroupProps = Omit<CheckboxGroupPrimitive.Props, "className" | "render"> & {
|
|
14
|
+
/** What the options are about: "Show on each person". Read by screen readers as the group's name. */
|
|
15
|
+
label: React.ReactNode;
|
|
16
|
+
/** Layout only. */
|
|
17
|
+
className?: string;
|
|
18
|
+
};
|
|
19
|
+
/** A list of options, each a Checkbox with a `value` and a `label`. The group holds the ticked values. */
|
|
20
|
+
declare function CheckboxGroup({ label, className, children, ...props }: CheckboxGroupProps): React.JSX.Element;
|
|
21
|
+
export { Checkbox, CheckboxGroup };
|
|
22
|
+
export type { CheckboxGroupProps, CheckboxProps };
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { Checkbox as CheckboxPrimitive } from "@base-ui/react/checkbox";
|
|
5
|
+
import { CheckboxGroup as CheckboxGroupPrimitive } from "@base-ui/react/checkbox-group";
|
|
6
|
+
import { Check, Minus } from "lucide-react";
|
|
7
|
+
import { cn } from "@brdgapp/ui/lib/utils";
|
|
8
|
+
// A box people tick: choosing rows, turning on one option in a form, or picking several from a list.
|
|
9
|
+
// Started from shadcn's Base UI checkbox, then put on Bridge's look.
|
|
10
|
+
//
|
|
11
|
+
// Decisions (2026-11-03, after checking all 12 app repos: about 120 checkboxes, most in raise and
|
|
12
|
+
// app-directories):
|
|
13
|
+
// - 16px box, 4px corners: the shadcn size every app but Bridge OS uses. Bridge OS's 14px boxes are the
|
|
14
|
+
// ones the Selection pattern flags as too small to hit.
|
|
15
|
+
// - The empty box's outline is --text-muted, not a border token: --border-default is 1.2:1 on white, and a
|
|
16
|
+
// control's outline needs 3:1. Ticked and part-ticked fill with --primary-solid, like Switch.
|
|
17
|
+
// - Every box has a 28px hit area (Bridge's smallest target), reaching past it invisibly.
|
|
18
|
+
// - "Part-ticked" (`indeterminate`) for a "Select all" box when only some rows are chosen, as the
|
|
19
|
+
// Selection pattern asks. app-cc draws this by hand in 12 places.
|
|
20
|
+
// - `label` and `description` for the one-option-in-a-form case ("I currently work here"), and
|
|
21
|
+
// CheckboxGroup for a list of options (app-directories' column options, raise's view settings).
|
|
22
|
+
const boxClasses = cn("relative inline-flex size-4 shrink-0 items-center justify-center rounded-[4px] border border-[var(--text-muted)] bg-[var(--surface-primary)] text-white", "transition-colors duration-150 ease-out outline-none", "after:absolute after:-inset-1.5 after:content-['']", "hover:border-[var(--text-secondary)]", "focus-visible:ring-2 focus-visible:ring-[var(--primary)] focus-visible:ring-offset-1 focus-visible:ring-offset-[var(--surface-primary)]", "data-[checked]:border-[var(--primary-solid)] data-[checked]:bg-[var(--primary-solid)]", "data-[indeterminate]:border-[var(--primary-solid)] data-[indeterminate]:bg-[var(--primary-solid)]", "data-[checked]:hover:border-[var(--primary-solid-hover)] data-[checked]:hover:bg-[var(--primary-solid-hover)]", "data-[indeterminate]:hover:border-[var(--primary-solid-hover)] data-[indeterminate]:hover:bg-[var(--primary-solid-hover)]", "aria-invalid:border-[var(--error)]", "data-[disabled]:cursor-not-allowed data-[disabled]:opacity-50");
|
|
23
|
+
function Checkbox({ label, description, className, disabled, ...props }) {
|
|
24
|
+
const descriptionId = React.useId();
|
|
25
|
+
const box = (_jsx(CheckboxPrimitive.Root, { "data-slot": "checkbox", disabled: disabled, "aria-describedby": description ? descriptionId : undefined, className: cn(boxClasses, !label && className, label && "mt-0.5"), ...props, children: _jsx(CheckboxPrimitive.Indicator, { "data-slot": "checkbox-indicator", keepMounted: true, className: "flex data-[unchecked]:hidden", render: (indicatorProps, state) => (_jsx("span", { ...indicatorProps, children: state.indeterminate ? _jsx(Minus, { className: "size-3", strokeWidth: 3 }) : _jsx(Check, { className: "size-3", strokeWidth: 3 }) })) }) }));
|
|
26
|
+
if (!label)
|
|
27
|
+
return box;
|
|
28
|
+
return (_jsxs("label", { "data-slot": "checkbox-label", className: cn("flex w-fit items-start gap-2.5", disabled ? "cursor-not-allowed opacity-50" : "cursor-pointer", className), children: [box, _jsxs("span", { className: "flex flex-col gap-0.5", children: [_jsx("span", { className: "text-[14px] leading-5 text-[var(--text-primary)]", children: label }), description ? (_jsx("span", { id: descriptionId, className: "text-[12px] leading-4 text-[var(--text-muted)]", children: description })) : null] })] }));
|
|
29
|
+
}
|
|
30
|
+
/** A list of options, each a Checkbox with a `value` and a `label`. The group holds the ticked values. */
|
|
31
|
+
function CheckboxGroup({ label, className, children, ...props }) {
|
|
32
|
+
const labelId = React.useId();
|
|
33
|
+
return (_jsxs(CheckboxGroupPrimitive, { "data-slot": "checkbox-group", "aria-labelledby": labelId, className: cn("flex flex-col gap-3", className), ...props, children: [_jsx("span", { id: labelId, className: "text-[14px] font-medium text-[var(--text-primary)]", children: label }), children] }));
|
|
34
|
+
}
|
|
35
|
+
export { Checkbox, CheckboxGroup };
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Collapsible as CollapsiblePrimitive } from "@base-ui/react/collapsible";
|
|
2
|
+
import { type VariantProps } from "class-variance-authority";
|
|
3
|
+
import type { ReactNode } from "react";
|
|
4
|
+
declare const triggerVariants: (props?: ({
|
|
5
|
+
variant?: "section" | "inline" | null | undefined;
|
|
6
|
+
} & import("class-variance-authority/types").ClassProp) | undefined) => string;
|
|
7
|
+
type CollapsibleProps = Omit<CollapsiblePrimitive.Root.Props, "className" | "render"> & {
|
|
8
|
+
/** Layout only. */
|
|
9
|
+
className?: string;
|
|
10
|
+
};
|
|
11
|
+
/** Holds whether it's open. Put a CollapsibleTrigger and a CollapsibleContent inside. */
|
|
12
|
+
declare function Collapsible({ className, ...props }: CollapsibleProps): import("react").JSX.Element;
|
|
13
|
+
type CollapsibleTriggerProps = Omit<CollapsiblePrimitive.Trigger.Props, "className" | "render" | "children"> & VariantProps<typeof triggerVariants> & {
|
|
14
|
+
/** The words on the trigger: "Advanced", "History on this list". */
|
|
15
|
+
children: ReactNode;
|
|
16
|
+
/** `section` only: a number shown before the arrow, like how many items are inside. */
|
|
17
|
+
count?: number;
|
|
18
|
+
/** Layout only. */
|
|
19
|
+
className?: string;
|
|
20
|
+
};
|
|
21
|
+
declare function CollapsibleTrigger({ variant, count, className, children, ...props }: CollapsibleTriggerProps): import("react").JSX.Element;
|
|
22
|
+
type CollapsibleContentProps = Omit<CollapsiblePrimitive.Panel.Props, "className" | "render"> & {
|
|
23
|
+
/** Layout only: `pt-3`, `pl-5`. */
|
|
24
|
+
className?: string;
|
|
25
|
+
};
|
|
26
|
+
/** What opens. Slides open and shut; content inside keeps its own spacing. */
|
|
27
|
+
declare function CollapsibleContent({ className, children, ...props }: CollapsibleContentProps): import("react").JSX.Element;
|
|
28
|
+
export { Collapsible, CollapsibleContent, CollapsibleTrigger, triggerVariants as collapsibleTriggerVariants };
|
|
29
|
+
export type { CollapsibleContentProps, CollapsibleProps, CollapsibleTriggerProps };
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Collapsible as CollapsiblePrimitive } from "@base-ui/react/collapsible";
|
|
4
|
+
import { cva } from "class-variance-authority";
|
|
5
|
+
import { ChevronDown, ChevronRight } from "lucide-react";
|
|
6
|
+
import { cn } from "@brdgapp/ui/lib/utils";
|
|
7
|
+
// A section people open and close: "Advanced", "History on this list". Started from shadcn's Base UI
|
|
8
|
+
// collapsible, then given Bridge's two looks.
|
|
9
|
+
//
|
|
10
|
+
// Decisions (2026-11-03, after checking all 12 app repos: about 40 hand-made open/close sections):
|
|
11
|
+
// - `section`: title on the left, a down arrow on the right that flips up when open. 28 files, among them
|
|
12
|
+
// Bridge OS's "Advanced" in the account drawer and app-perks' Disclosure.
|
|
13
|
+
// - `inline`: a small right arrow before the words, turning down when open. 13 files, among them raise's
|
|
14
|
+
// "History on this list" and app-portfolio's expandable rows.
|
|
15
|
+
// - Opens by sliding, 200ms, so what's below moves smoothly instead of jumping. Base UI measures the
|
|
16
|
+
// height. Off with reduced motion.
|
|
17
|
+
const triggerVariants = cva("group/collapsible-trigger inline-flex items-center rounded text-left outline-none transition-colors duration-150 ease-out focus-visible:ring-2 focus-visible:ring-[var(--primary)] disabled:cursor-not-allowed disabled:opacity-50", {
|
|
18
|
+
variants: {
|
|
19
|
+
variant: {
|
|
20
|
+
section: "w-full justify-between gap-3 py-1 text-[13px] font-bold text-[var(--text-primary)]",
|
|
21
|
+
inline: "-mx-1 gap-1 px-1 text-[13px] font-medium text-[var(--text-secondary)] hover:text-[var(--text-primary)]",
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
defaultVariants: { variant: "section" },
|
|
25
|
+
});
|
|
26
|
+
/** Holds whether it's open. Put a CollapsibleTrigger and a CollapsibleContent inside. */
|
|
27
|
+
function Collapsible({ className, ...props }) {
|
|
28
|
+
return _jsx(CollapsiblePrimitive.Root, { "data-slot": "collapsible", className: className, ...props });
|
|
29
|
+
}
|
|
30
|
+
function CollapsibleTrigger({ variant = "section", count, className, children, ...props }) {
|
|
31
|
+
const arrow = "size-4 shrink-0 text-[var(--text-muted)] transition-transform duration-200 ease-out motion-reduce:transition-none";
|
|
32
|
+
return (_jsx(CollapsiblePrimitive.Trigger, { "data-slot": "collapsible-trigger", "data-variant": variant, className: cn(triggerVariants({ variant }), className), ...props, children: variant === "inline" ? (_jsxs(_Fragment, { children: [_jsx(ChevronRight, { "aria-hidden": "true", className: cn(arrow, "group-data-[panel-open]/collapsible-trigger:rotate-90") }), children] })) : (_jsxs(_Fragment, { children: [_jsx("span", { className: "min-w-0 flex-1", children: children }), count !== undefined ? (_jsx("span", { className: "text-[12px] font-medium tabular-nums text-[var(--text-muted)]", children: count.toLocaleString("en-US") })) : null, _jsx(ChevronDown, { "aria-hidden": "true", className: cn(arrow, "group-hover/collapsible-trigger:text-[var(--text-primary)] group-data-[panel-open]/collapsible-trigger:rotate-180") })] })) }));
|
|
33
|
+
}
|
|
34
|
+
/** What opens. Slides open and shut; content inside keeps its own spacing. */
|
|
35
|
+
function CollapsibleContent({ className, children, ...props }) {
|
|
36
|
+
return (_jsx(CollapsiblePrimitive.Panel, { "data-slot": "collapsible-content", className: cn("h-[var(--collapsible-panel-height)] overflow-hidden transition-[height] duration-200 ease-out motion-reduce:transition-none", "data-[starting-style]:h-0 data-[ending-style]:h-0"), ...props, children: _jsx("div", { className: className, children: children }) }));
|
|
37
|
+
}
|
|
38
|
+
export { Collapsible, CollapsibleContent, CollapsibleTrigger, triggerVariants as collapsibleTriggerVariants };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type LucideIcon } from "lucide-react";
|
|
2
|
+
import { type FilterPillOption } from "@brdgapp/ui/components/filter-pill";
|
|
3
|
+
import { type ElementProps } from "@brdgapp/ui/lib/utils";
|
|
4
|
+
type ColumnFilterMenuProps<T extends string> = ElementProps<HTMLButtonElement> & {
|
|
5
|
+
icon: LucideIcon;
|
|
6
|
+
label: string;
|
|
7
|
+
/** Explains the column. Hidden while the menu is open. */
|
|
8
|
+
tooltip?: string;
|
|
9
|
+
value: T;
|
|
10
|
+
options: FilterPillOption<T>[];
|
|
11
|
+
onChange: (value: T) => void;
|
|
12
|
+
};
|
|
13
|
+
declare function ColumnFilterMenu<T extends string>({ icon: Icon, label, tooltip, value, options, onChange, ...rest }: ColumnFilterMenuProps<T>): import("react").JSX.Element;
|
|
14
|
+
export { ColumnFilterMenu };
|
|
15
|
+
export type { ColumnFilterMenuProps };
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { Menu } from "@base-ui/react/menu";
|
|
5
|
+
import { ChevronDown } from "lucide-react";
|
|
6
|
+
import { FilterMenuPopup, FilterSheet } from "@brdgapp/ui/components/filter-pill";
|
|
7
|
+
import { useIsMobile } from "@brdgapp/ui/hooks/use-is-mobile";
|
|
8
|
+
import { Tip } from "@brdgapp/ui/components/info-hint";
|
|
9
|
+
import { cn } from "@brdgapp/ui/lib/utils";
|
|
10
|
+
function ColumnFilterMenu({ icon: Icon, label, tooltip, value, options, onChange, ...rest }) {
|
|
11
|
+
const [open, setOpen] = useState(false);
|
|
12
|
+
const mobile = useIsMobile();
|
|
13
|
+
const active = options.length > 0 && value !== options[0]?.value;
|
|
14
|
+
const activeLabel = options.find((o) => o.value === value)?.label;
|
|
15
|
+
const name = `Filter by ${label.toLowerCase()}${active && activeLabel ? `: ${activeLabel}` : ""}`;
|
|
16
|
+
const triggerClass = cn("-mx-1.5 inline-flex items-center gap-1.5 rounded px-1.5 py-0.5 text-[12px] font-bold whitespace-nowrap outline-none transition-colors", "hover:bg-[var(--surface-tertiary)] focus-visible:ring-2 focus-visible:ring-[var(--primary)]", active ? "text-[var(--primary)]" : "text-[var(--text-secondary)]");
|
|
17
|
+
const inner = (_jsxs(_Fragment, { children: [_jsx(Icon, { "aria-hidden": "true", className: cn("size-3.5", active ? "text-[var(--primary)]" : "text-[var(--text-muted)]") }), label, active ? _jsx("span", { "aria-hidden": "true", className: "size-1.5 rounded-full bg-[var(--primary)]" }) : null, _jsx(ChevronDown, { "aria-hidden": "true", className: cn("size-3.5 text-[var(--text-muted)] transition-transform", open && "rotate-180") })] }));
|
|
18
|
+
// Phones: a bottom sheet instead of a menu. No tooltip: touch has no hover.
|
|
19
|
+
if (mobile) {
|
|
20
|
+
return (_jsxs(_Fragment, { children: [_jsx("button", { ...rest, type: "button", "data-slot": "column-filter-menu", "aria-haspopup": "dialog", "aria-label": name, className: triggerClass, onClick: (e) => {
|
|
21
|
+
// A header often sits in a clickable row; keep the click here.
|
|
22
|
+
e.stopPropagation();
|
|
23
|
+
setOpen(true);
|
|
24
|
+
}, children: inner }), _jsx(FilterSheet, { open: open, onClose: () => setOpen(false), title: label, value: value, options: options, onChange: onChange })] }));
|
|
25
|
+
}
|
|
26
|
+
return (_jsxs(Menu.Root, { open: open, onOpenChange: setOpen, children: [_jsx(Tip, { text: tooltip, disabled: open, children: _jsx(Menu.Trigger, { ...rest, "data-slot": "column-filter-menu", "aria-label": name,
|
|
27
|
+
// A header often sits in a clickable row; keep the click here.
|
|
28
|
+
onClick: (e) => e.stopPropagation(), className: triggerClass, children: inner }) }), _jsx(FilterMenuPopup, { value: value, options: options, onChange: onChange, width: 220, align: "start" })] }));
|
|
29
|
+
}
|
|
30
|
+
export { ColumnFilterMenu };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type LucideIcon } from "lucide-react";
|
|
2
|
+
import { type ElementProps } from "@brdgapp/ui/lib/utils";
|
|
3
|
+
type ColumnSortButtonProps = ElementProps<HTMLButtonElement> & {
|
|
4
|
+
icon: LucideIcon;
|
|
5
|
+
label: string;
|
|
6
|
+
tooltip?: string;
|
|
7
|
+
/** This column's sort, or null when another column is the sort. */
|
|
8
|
+
direction: "asc" | "desc" | null;
|
|
9
|
+
/** Words for each direction, read by screen readers: { asc: "lowest first", desc: "highest first" }. */
|
|
10
|
+
describe?: {
|
|
11
|
+
asc: string;
|
|
12
|
+
desc: string;
|
|
13
|
+
};
|
|
14
|
+
onClick: () => void;
|
|
15
|
+
};
|
|
16
|
+
declare function ColumnSortButton({ icon: Icon, label, tooltip, direction, describe, onClick, ...rest }: ColumnSortButtonProps): import("react").JSX.Element;
|
|
17
|
+
export { ColumnSortButton };
|
|
18
|
+
export type { ColumnSortButtonProps };
|