@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
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
Every release of `@brdgapp/ui`. Versions follow the rules in README.md → Versions.
|
|
4
|
+
|
|
5
|
+
## 1.0.0
|
|
6
|
+
|
|
7
|
+
The first release of Bridge's design system as a package. A new major version: the 0.1.x releases
|
|
8
|
+
(NetworkPeopleHelper, BridgeUIProvider) are not part of it. Apps using them stay on 0.1.2 until they move
|
|
9
|
+
those parts into their own code.
|
|
10
|
+
|
|
11
|
+
- Tokens: every Bridge role token in light and dark, three shadow levels, glass, dot-grid and noise
|
|
12
|
+
utilities, and base styles (`globals.css`).
|
|
13
|
+
- Components: ActionMenu, Avatar, Badge, BottomSheet, BrandMark, Button, ButtonGroup, Card (with CardHeader, CardMedia and CardFooter), Checkbox (with CheckboxGroup), Collapsible, HoverCard, Illustration, NetworkGraph, Scene, ColumnFilterMenu, ColumnSortButton, ConfirmDialog,
|
|
14
|
+
CopyField, DataState, DataTable, Dialog, Drawer, ExpandableBio, Field (with FieldLabel, FieldDescription,
|
|
15
|
+
FieldError and FieldGroup), FilterChip, FilterPill, IconButton, InfoHint (with HoverTip and Tip), Input,
|
|
16
|
+
PageBanner, SearchInput, SegmentedControl, Skeleton, SkipLink, StatBox, Switch, Tabs, ThemeSwitch, Tag, TagInput, Textarea, Toast.
|
|
17
|
+
- Needs React 19 and Tailwind 4 (the components take `ref` as a prop and use `inert`, which React 18 ignores).
|
|
18
|
+
- Built on Base UI. Each component's handbook page notes how it relates to Bridge OS's own version.
|
|
19
|
+
- Avatar shows a person's photo, or their initials on light blue, in six sizes (16 to 64px), with an
|
|
20
|
+
optional badge: Bridge member, Super Connector or warning. A photo that fails to load falls back to
|
|
21
|
+
the initials. AvatarGroup stacks the first faces and counts the rest ("+4").
|
|
22
|
+
- Textarea starts 3 lines tall, grows with the text up to 10, then scrolls. With `maxLength` it shows a
|
|
23
|
+
count under the box that turns red near the limit.
|
|
24
|
+
- Checkbox is 16px with a 28px click area, can be part-ticked for "Select all", and takes a `label` and
|
|
25
|
+
`description`. CheckboxGroup holds a list of ticked options.
|
|
26
|
+
- Collapsible opens and closes a section in two looks (`section`, `inline`), sliding open in 200ms.
|
|
27
|
+
- BrandMark draws the company logos Bridge shows (LinkedIn, Google, Microsoft, Apple, WhatsApp, Telegram,
|
|
28
|
+
Chrome) at icon size, in their own colors; LinkedIn, WhatsApp and Telegram can be one color. Lucide 1.x has no logos.
|
|
29
|
+
- Illustration draws thirteen small scenes for empty states (people, intros, companies, messages, activity,
|
|
30
|
+
meetings, lists, jobs, connect, search, filters, done, error) in tokens, so they follow dark mode. DataState's
|
|
31
|
+
empty state takes `illustration` in place of `icon`.
|
|
32
|
+
- NetworkGraph shows you (or someone) in the middle and the people you know around you, with an optional blue path.
|
|
33
|
+
- Scene frames a product picture built from real components: decorative, inert, on the dot grid or a blue wash.
|
|
34
|
+
- Light, Dark or System in every app: `themeScript` (`@brdgapp/ui/lib/theme`) sets the theme before the page
|
|
35
|
+
paints; `useTheme()` (`@brdgapp/ui/hooks/use-theme`) reads and changes it; ThemeSwitch is the Appearance setting.
|
|
36
|
+
System is the default. The choice is a cookie for all of brdg.app, so every Bridge app follows it; Bridge OS's
|
|
37
|
+
old saved choice is still read.
|
|
38
|
+
- Focus is a solid 2px blue ring (red on danger buttons, white on blue) with a 2px gap: 7:1 in light, 5:1 in dark.
|
|
39
|
+
Bridge OS's 40% ring was under WCAG's 3:1. A fallback outline in `globals.css` covers anything an app builds
|
|
40
|
+
itself. TabPanel shows the ring when it takes focus.
|
|
41
|
+
- Disabled is 50% in every component (Bridge OS buttons used 30%; the package mixed 30, 40, 50 and 60).
|
|
42
|
+
- `glass-chrome` is the full top-bar glass (a 40px blur, a hairline under it, a near-solid fill where Safari
|
|
43
|
+
drops the blur). `liquid-glass-sm`, used nowhere, is removed. The stylesheet needs Tailwind 4.
|
|
44
|
+
- Overlays follow Motion's speeds on Bridge's curve: menus, tips, hover cards and toasts 200ms in and 150ms out;
|
|
45
|
+
dialogs, drawers, sheets and backdrops 300ms in and 200ms out (dialogs and menus were 150ms both ways).
|
|
46
|
+
- SkipLink: “Skip to content”, hidden until focused, for every app's layout.
|
|
47
|
+
- HoverCard previews a person or company after the pointer rests on its name for 300ms. Read-only.
|
|
48
|
+
- Card is frosted glass in three sizes (`lg`, `md`, `sm`), or a `bare` row inside another card. It can be a link
|
|
49
|
+
(`href`, rises on hover) or picked from a list (`onSelect`, `selected`, tints on hover); controls inside
|
|
50
|
+
keep working with no extra code. Header with icon (in blue, green, amber or purple) or avatar, an ⓘ hint, meta, action and an optional strip; CardMedia;
|
|
51
|
+
CardFooter; `flush` for tables.
|
|
52
|
+
- `--text-muted` is #6D7284 (Bridge OS #6E7385) so it passes 4.5:1 on `--surface-secondary`; dark `--purple` is
|
|
53
|
+
#9A81ED (Bridge OS #8B6EE8) so purple tags pass 4.5:1 on every dark surface.
|
|
54
|
+
- `cn` knows Bridge's page textures (`bg-dot-grid`, `bg-noise`, `bg-glow-patches`) aren't background colors, so
|
|
55
|
+
it keeps both a color and a texture, and lets the three stack.
|
|
56
|
+
- Named type sizes on one ladder: `text-11`, `-12`, `-13`, `-14`, `-16`, `-18`, `-22`, `-28`, `-36`, `-48`, each with its
|
|
57
|
+
line height. `cn` knows they are sizes. Card titles are 16px; Dialog, ConfirmDialog and Drawer titles 18px.
|
|
58
|
+
Input, Textarea, SearchInput and TagInput are 16px on phones (14px from 768px up) so iOS doesn't zoom in.
|
|
59
|
+
- Button `md` is 40px tall (Bridge OS 38px), the height of a text field, so the two line up in a row.
|
|
60
|
+
- `@brdgapp/ui/lib/layout`: `PAGE_WIDTH` (wide 1600, standard 1280, read 800, form 720, narrow 448), `PAGE_EDGES`,
|
|
61
|
+
`pageFrame(width)` and `RAIL` (a 340px side rail from 1024px).
|
|
62
|
+
- Phones (below 768px): FilterPill and ColumnFilterMenu open as a BottomSheet; ActionMenu rows, Tabs, fields and
|
|
63
|
+
the medium button are 44px tall; CopyField's copy hint shows without hover. `useIsMobile()` in
|
|
64
|
+
`@brdgapp/ui/hooks/use-is-mobile`.
|
|
65
|
+
- Corners: ColumnSortButton, ColumnFilterMenu and Collapsible triggers use 4px (were 6px), so every component
|
|
66
|
+
is on the five corners (4, 8, 12, 16, round).
|
|
67
|
+
- BottomSheet casts the dialog shadow (upward), not the drawer's.
|
|
68
|
+
- Motion: `ease-out` is Bridge's one curve, cubic-bezier(0.23, 1, 0.32, 1); `@brdgapp/ui/lib/motion` has the three
|
|
69
|
+
speeds (`DURATION`: 150, 200, 300ms), `EASE` and `transition()` for JavaScript animations.
|
|
70
|
+
- Tokens: `--super-connector-from`, `--super-connector-to` and `--badge-warning` for avatar badges.
|
|
71
|
+
- StatBox corners are 12px (the small-card radius), up from Bridge OS's 8px.
|
|
72
|
+
- Tag, TagInput, FilterPill, CopyField, ColumnSortButton, ColumnFilterMenu, InfoHint and HoverTip pass
|
|
73
|
+
extra attributes (`id`, `aria-*`, `data-*`, event handlers) to their main element; DataTable takes
|
|
74
|
+
`rowProps` for each row. The components' own props win where they overlap.
|
|
75
|
+
- `@brdgapp/ui/lib/format`: `formatCount`, `formatMoney`, `formatPercent`, `formatDate`, `formatTime`, Bridge's
|
|
76
|
+
writing rules for numbers and dates (American English). StatBox formats its value with `formatCount`
|
|
77
|
+
(1,284 in full; 12.5K from 10,000).
|
|
78
|
+
- Toast is built on Base UI Toast: mount `<Toaster />` once, then call `toast.success()` or
|
|
79
|
+
`toast.error()` from anywhere (a queue, a 3-second timer that pauses on hover, swipe to dismiss).
|
|
80
|
+
- SearchInput, TagInput, CopyField, ColumnSortButton and DataTable's page buttons are built on Base UI
|
|
81
|
+
Input and Button, like Button.
|
|
82
|
+
- Deprecated from the start: `Tag`'s `title` (a native tooltip). Use `Tip`; `title` is removed in 2.0.
|
|
83
|
+
- Deprecated from the start: `<Toast open message />`, the Bridge OS API. Use `toast`; it is removed in 2.0.
|
package/README.md
CHANGED
|
@@ -1,113 +1,67 @@
|
|
|
1
1
|
# @brdgapp/ui
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Bridge's shared components, tokens and styles. How and when to use each part is in the handbook:
|
|
4
|
+
**https://handbook.brdg.app** (start with "Using the system").
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
## Needs
|
|
6
7
|
|
|
7
|
-
|
|
8
|
+
- React 19
|
|
9
|
+
- Tailwind CSS 4 (apps still on Tailwind 3 upgrade first)
|
|
10
|
+
- The Mulish font, loaded by the app
|
|
8
11
|
|
|
9
|
-
##
|
|
10
|
-
|
|
11
|
-
Peer dependencies are `react` and `react-dom` (18 or newer). The app already has those.
|
|
12
|
+
## Set up
|
|
12
13
|
|
|
13
14
|
```bash
|
|
14
|
-
|
|
15
|
-
```
|
|
16
|
-
|
|
17
|
-
The package publishes TypeScript source, not a prebuilt `dist`. The app bundler must compile it.
|
|
18
|
-
|
|
19
|
-
### Next.js
|
|
20
|
-
|
|
21
|
-
In `next.config.mjs` (or `next.config.js`):
|
|
22
|
-
|
|
23
|
-
```js
|
|
24
|
-
const nextConfig = {
|
|
25
|
-
transpilePackages: ["@brdgapp/ui"],
|
|
26
|
-
}
|
|
15
|
+
npm install @brdgapp/ui
|
|
27
16
|
```
|
|
28
17
|
|
|
29
|
-
|
|
18
|
+
In the app's main CSS file, after Tailwind:
|
|
30
19
|
|
|
31
|
-
```
|
|
32
|
-
import "
|
|
20
|
+
```css
|
|
21
|
+
@import "tailwindcss";
|
|
22
|
+
@import "@brdgapp/ui/globals.css";
|
|
33
23
|
```
|
|
34
24
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
Add the package to `optimizeDeps.exclude` only if the dev server fails to compile its TypeScript. The package entry imports `styles.css`, so a normal Vite app gets the CSS from the JS import. If styles are missing, import `@brdgapp/ui/styles.css` once from the app entry.
|
|
38
|
-
|
|
39
|
-
## Provider
|
|
40
|
-
|
|
41
|
-
Wrap the tree once with `BridgeUIProvider`. It supplies the Bridge API base and the signed-in user’s access token. Components that talk to the Bridge API use this provider. They do not take their own token prop.
|
|
42
|
-
|
|
43
|
-
```tsx
|
|
44
|
-
"use client"
|
|
25
|
+
That brings every token (light and dark), the base styles, and tells Tailwind to read the package's
|
|
26
|
+
files, so its classes are included.
|
|
45
27
|
|
|
46
|
-
|
|
28
|
+
Load Mulish however the app loads fonts. With `next/font`, name the variable `--font-mulish`:
|
|
47
29
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
<BridgeUIProvider getAccessToken={getAuthToken}>
|
|
51
|
-
{children}
|
|
52
|
-
</BridgeUIProvider>
|
|
53
|
-
)
|
|
54
|
-
}
|
|
30
|
+
```ts
|
|
31
|
+
const mulish = Mulish({ subsets: ["latin"], variable: "--font-mulish" });
|
|
55
32
|
```
|
|
56
33
|
|
|
57
|
-
|
|
34
|
+
Anything else (a Google Fonts link, a self-hosted file) works too, as long as the family is "Mulish".
|
|
58
35
|
|
|
59
|
-
|
|
36
|
+
Themes follow `data-theme="light"` or `data-theme="dark"` on `<html>`, not the device setting.
|
|
60
37
|
|
|
61
|
-
|
|
62
|
-
"use server"
|
|
38
|
+
## Use
|
|
63
39
|
|
|
64
|
-
|
|
40
|
+
Import each component from its own file. There is no single index file.
|
|
65
41
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
}
|
|
42
|
+
```tsx
|
|
43
|
+
import { Button } from "@brdgapp/ui/components/button";
|
|
44
|
+
import { DataState } from "@brdgapp/ui/components/data-state";
|
|
45
|
+
import { cn } from "@brdgapp/ui/lib/utils";
|
|
70
46
|
```
|
|
71
47
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
`introInboxUrl` and `profileUrlForUsername` override the URLs used by built-in intro and profile actions. Defaults are `https://intros.brdg.app/inbox` and `https://os.brdg.app/to/<username>`.
|
|
75
|
-
|
|
76
|
-
## Components
|
|
77
|
-
|
|
78
|
-
| Component | Folder | What it is for |
|
|
79
|
-
| --- | --- | --- |
|
|
80
|
-
| Network people helper | [`src/network-people-helper`](./src/network-people-helper/README.md) | Shows how many people you know at a domain, and who they are, from the signed-in user’s Bridge network. |
|
|
81
|
-
|
|
82
|
-
Read that component’s README before rendering it. Import from `@brdgapp/ui`, not from a deep path.
|
|
48
|
+
For toasts, mount `<Toaster />` once in the root layout, then call `toast.success("Link copied")` from
|
|
49
|
+
anywhere (`@brdgapp/ui/components/toast`).
|
|
83
50
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
1. Add `src/<component-name>/` with the component source and a `README.md` that says what it is for, its props, and a usage example.
|
|
87
|
-
2. Export the public components and types from `src/<component-name>/index.ts`.
|
|
88
|
-
3. Re-export those names from `src/index.ts` so apps keep importing `@brdgapp/ui`.
|
|
89
|
-
4. If the component has CSS, put it in `src/<component-name>/styles.css` and add `@import "./<component-name>/styles.css";` to `src/styles.css`.
|
|
90
|
-
5. If it calls the Bridge API, use `useBridgeUI()` from `src/provider.tsx`. Do not read tokens inside the component.
|
|
91
|
-
|
|
92
|
-
## Publish
|
|
93
|
-
|
|
94
|
-
From this repo, after the version in `package.json` is the one you want to release:
|
|
95
|
-
|
|
96
|
-
```bash
|
|
97
|
-
pnpm login
|
|
98
|
-
pnpm publish
|
|
99
|
-
```
|
|
51
|
+
`useMockQuery` (`hooks/use-mock-query`) and `lib/demo-controls` are for handbook examples and
|
|
52
|
+
prototypes. Replace them with real data loading in product code.
|
|
100
53
|
|
|
101
|
-
|
|
54
|
+
## Versions
|
|
102
55
|
|
|
103
|
-
|
|
56
|
+
- A fix bumps the last number (1.0.1).
|
|
57
|
+
- A new component or option bumps the middle number (1.1.0).
|
|
58
|
+
- Anything that could break an app bumps the first number (2.0.0).
|
|
59
|
+
- Before something is renamed or removed, it's marked Deprecated for at least one release, with its
|
|
60
|
+
replacement named.
|
|
104
61
|
|
|
105
|
-
|
|
62
|
+
Every release is listed in `CHANGELOG.md`.
|
|
106
63
|
|
|
107
|
-
##
|
|
64
|
+
## Agents
|
|
108
65
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
3. Import `@brdgapp/ui/styles.css` once in the root layout.
|
|
112
|
-
4. Wrap the app in `BridgeUIProvider` with a `getAccessToken` that returns the signed-in user’s bearer token.
|
|
113
|
-
5. Open the README for the component you are adding and follow that.
|
|
66
|
+
AI coding agents connect to the handbook's read-only MCP server at `https://handbook.brdg.app/mcp`.
|
|
67
|
+
Setup: https://handbook.brdg.app/for-agents.
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type * as React from "react";
|
|
2
|
+
declare const ACTION_MENU_ITEM_CLASS: string;
|
|
3
|
+
type ActionMenuContextValue = {
|
|
4
|
+
close: (restoreFocus?: boolean) => void;
|
|
5
|
+
};
|
|
6
|
+
/** Close the menu from inside its body, for custom controls. */
|
|
7
|
+
declare function useActionMenu(): ActionMenuContextValue;
|
|
8
|
+
/** Everything the trigger needs: ref, click and keyboard handlers, aria-haspopup, aria-expanded. */
|
|
9
|
+
type TriggerRenderProps = React.ButtonHTMLAttributes<HTMLButtonElement> & {
|
|
10
|
+
ref?: React.Ref<HTMLButtonElement>;
|
|
11
|
+
"aria-haspopup"?: React.AriaAttributes["aria-haspopup"];
|
|
12
|
+
"aria-expanded"?: boolean;
|
|
13
|
+
};
|
|
14
|
+
type ActionMenuProps = {
|
|
15
|
+
/** Names the default ⋮ trigger. Name what the menu is for when it helps: "Actions for Leah Okonkwo". */
|
|
16
|
+
ariaLabel?: string;
|
|
17
|
+
/** Trigger size: `sm` for compact rows, `md` for headers. */
|
|
18
|
+
size?: "sm" | "md";
|
|
19
|
+
/** Menu width in px. 248 is the contact-menu width. */
|
|
20
|
+
width?: number;
|
|
21
|
+
/** Trigger icon. Defaults to vertical dots. */
|
|
22
|
+
icon?: React.ReactNode;
|
|
23
|
+
triggerClassName?: string;
|
|
24
|
+
menuClassName?: string;
|
|
25
|
+
/** Stop the trigger's click reaching a clickable row behind it. */
|
|
26
|
+
stopPropagation?: boolean;
|
|
27
|
+
/** Know when it opens, for example to load data then. */
|
|
28
|
+
onOpenChange?: (open: boolean) => void;
|
|
29
|
+
/**
|
|
30
|
+
* A custom trigger, such as a split button's caret. Spread `props` onto your control so it gets the
|
|
31
|
+
* keyboard handling too (Arrow Down opens the menu). `state.open` says whether the menu is open.
|
|
32
|
+
*/
|
|
33
|
+
renderTrigger?: (props: TriggerRenderProps, state: {
|
|
34
|
+
open: boolean;
|
|
35
|
+
}) => React.ReactElement;
|
|
36
|
+
children: React.ReactNode | ((close: (restoreFocus?: boolean) => void) => React.ReactNode);
|
|
37
|
+
};
|
|
38
|
+
declare function ActionMenu({ ariaLabel, size, width, icon, triggerClassName, menuClassName, stopPropagation, onOpenChange, renderTrigger, children, }: ActionMenuProps): React.JSX.Element;
|
|
39
|
+
type ActionMenuItemProps = {
|
|
40
|
+
icon?: React.ReactNode;
|
|
41
|
+
children: React.ReactNode;
|
|
42
|
+
/** A muted second line under the label. */
|
|
43
|
+
description?: React.ReactNode;
|
|
44
|
+
onClick?: () => void;
|
|
45
|
+
/** Makes the row a link. */
|
|
46
|
+
href?: string;
|
|
47
|
+
/** With `href`: opens in a new tab. */
|
|
48
|
+
external?: boolean;
|
|
49
|
+
/** Red, for removing or leaving. */
|
|
50
|
+
danger?: boolean;
|
|
51
|
+
disabled?: boolean;
|
|
52
|
+
/** Keep the menu open after choosing, for a row that shows its own result ("Copied"). */
|
|
53
|
+
closeOnClick?: boolean;
|
|
54
|
+
/**
|
|
55
|
+
* Render the link with your router's link, e.g. `render={<Link href="/settings" />}`.
|
|
56
|
+
* Without it, `href` renders a plain `<a>`.
|
|
57
|
+
*/
|
|
58
|
+
render?: React.ReactElement;
|
|
59
|
+
className?: string;
|
|
60
|
+
};
|
|
61
|
+
declare function ActionMenuItem({ icon, children, description, onClick, href, external, danger, disabled, closeOnClick, render, className, }: ActionMenuItemProps): React.JSX.Element;
|
|
62
|
+
/** A thin line between groups of rows. */
|
|
63
|
+
declare function ActionMenuSeparator(): React.JSX.Element;
|
|
64
|
+
export { ActionMenu, ActionMenuItem, ActionMenuSeparator, useActionMenu, ACTION_MENU_ITEM_CLASS };
|
|
65
|
+
export type { ActionMenuProps, ActionMenuItemProps };
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { createContext, useCallback, useContext, useMemo, useState } from "react";
|
|
4
|
+
import { Menu } from "@base-ui/react/menu";
|
|
5
|
+
import { MoreVertical } from "lucide-react";
|
|
6
|
+
import { IconButton } from "@brdgapp/ui/components/icon-button";
|
|
7
|
+
import { cn } from "@brdgapp/ui/lib/utils";
|
|
8
|
+
// THE overflow (⋮) menu. Ported from Bridge OS src/components/ui/action-menu.tsx with the same API.
|
|
9
|
+
// Bridge OS hand-built the portal, positioning, outside-click and Escape handling; Base UI Menu now
|
|
10
|
+
// does that, plus arrow-key movement, type-to-find and focus return. It also follows the trigger on
|
|
11
|
+
// scroll instead of closing. The danger colour is now a token (Bridge OS hard-coded #E13535).
|
|
12
|
+
// The one menu-row look. Exported so custom stateful rows can match it.
|
|
13
|
+
const ACTION_MENU_ITEM_CLASS = "flex w-full cursor-default items-center gap-3 px-3.5 py-2.5 max-md:min-h-11 text-left text-[13px] text-[var(--text-body)] outline-none transition-colors " +
|
|
14
|
+
"data-[highlighted]:bg-[var(--surface-secondary)] hover:bg-[var(--surface-secondary)] data-[disabled]:opacity-50 [&_svg]:size-4 [&_svg]:shrink-0";
|
|
15
|
+
const ACTION_MENU_ITEM_DANGER_CLASS = "text-[var(--danger-text)] data-[highlighted]:bg-[var(--error-bg)] hover:bg-[var(--error-bg)]";
|
|
16
|
+
const ActionMenuContext = createContext(null);
|
|
17
|
+
/** Close the menu from inside its body, for custom controls. */
|
|
18
|
+
function useActionMenu() {
|
|
19
|
+
const ctx = useContext(ActionMenuContext);
|
|
20
|
+
if (!ctx)
|
|
21
|
+
throw new Error("useActionMenu must be used inside <ActionMenu>");
|
|
22
|
+
return ctx;
|
|
23
|
+
}
|
|
24
|
+
function ActionMenu({ ariaLabel = "More options", size = "md", width = 248, icon, triggerClassName, menuClassName, stopPropagation = true, onOpenChange, renderTrigger, children, }) {
|
|
25
|
+
const [open, setOpen] = useState(false);
|
|
26
|
+
const setOpenAndNotify = useCallback((next) => {
|
|
27
|
+
setOpen(next);
|
|
28
|
+
onOpenChange?.(next);
|
|
29
|
+
}, [onOpenChange]);
|
|
30
|
+
// Base UI returns focus to the trigger whenever the menu closes, so `restoreFocus` needs no extra work.
|
|
31
|
+
const close = useCallback(() => setOpenAndNotify(false), [setOpenAndNotify]);
|
|
32
|
+
const ctx = useMemo(() => ({ close }), [close]);
|
|
33
|
+
const stop = stopPropagation ? (e) => e.stopPropagation() : undefined;
|
|
34
|
+
return (_jsxs(Menu.Root, { open: open, onOpenChange: setOpenAndNotify, children: [renderTrigger ? (_jsx(Menu.Trigger, { render: (props) => renderTrigger(props, { open }) })) : (_jsx(Menu.Trigger, { render: _jsx(IconButton, { "aria-label": ariaLabel, size: size, className: cn("shrink-0", triggerClassName), children: icon ?? _jsx(MoreVertical, {}) }), onClick: stop, onPointerDown: stop })), _jsx(Menu.Portal, { children: _jsx(Menu.Positioner, { side: "bottom", align: "end", sideOffset: 6, collisionPadding: 8, className: "z-[80]", children: _jsx(Menu.Popup, { "data-slot": "action-menu", style: { width }, className: cn("max-w-[calc(100vw-16px)] rounded-xl border border-[var(--border-default)] bg-[var(--surface-elevated)] py-1 shadow-[var(--shadow-menu)] outline-none", "origin-[var(--transform-origin)] transition-[opacity,scale] duration-200 ease-out data-[ending-style]:duration-150 data-[starting-style]:scale-95 data-[starting-style]:opacity-0 data-[ending-style]:opacity-0", menuClassName), children: _jsx(ActionMenuContext.Provider, { value: ctx, children: typeof children === "function" ? children(close) : children }) }) }) })] }));
|
|
35
|
+
}
|
|
36
|
+
function ActionMenuItem({ icon, children, description, onClick, href, external = false, danger = false, disabled = false, closeOnClick = true, render, className, }) {
|
|
37
|
+
const classes = cn(ACTION_MENU_ITEM_CLASS, danger && ACTION_MENU_ITEM_DANGER_CLASS, description && "items-start", className);
|
|
38
|
+
const body = description ? (_jsxs(_Fragment, { children: [_jsx("span", { "aria-hidden": "true", className: "mt-0.5 inline-flex shrink-0", children: icon }), _jsxs("span", { className: "min-w-0", children: [_jsx("span", { className: "block text-[13px] font-semibold text-[var(--text-primary)]", children: children }), _jsx("span", { className: "block text-[12px] text-[var(--text-muted)]", children: description })] })] })) : (_jsxs(_Fragment, { children: [icon ? (_jsx("span", { "aria-hidden": "true", className: "inline-flex shrink-0", children: icon })) : null, children] }));
|
|
39
|
+
if (href && !disabled) {
|
|
40
|
+
return (_jsx(Menu.LinkItem, { "data-slot": "action-menu-item", href: href, closeOnClick: closeOnClick, onClick: onClick, className: classes, render: render, ...(external ? { target: "_blank", rel: "noopener noreferrer" } : {}), children: body }));
|
|
41
|
+
}
|
|
42
|
+
return (_jsx(Menu.Item, { "data-slot": "action-menu-item", onClick: onClick, disabled: disabled, closeOnClick: closeOnClick, className: classes, children: body }));
|
|
43
|
+
}
|
|
44
|
+
/** A thin line between groups of rows. */
|
|
45
|
+
function ActionMenuSeparator() {
|
|
46
|
+
return _jsx(Menu.Separator, { "data-slot": "action-menu-separator", className: "my-1 h-px bg-[var(--border-divider)]" });
|
|
47
|
+
}
|
|
48
|
+
export { ActionMenu, ActionMenuItem, ActionMenuSeparator, useActionMenu, ACTION_MENU_ITEM_CLASS };
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { CSSProperties } from "react";
|
|
2
|
+
import { type VariantProps } from "class-variance-authority";
|
|
3
|
+
declare const avatarVariants: (props?: ({
|
|
4
|
+
size?: "sm" | "md" | "lg" | "xs" | "xl" | "2xl" | null | undefined;
|
|
5
|
+
} & import("class-variance-authority/types").ClassProp) | undefined) => string;
|
|
6
|
+
type AvatarSize = NonNullable<VariantProps<typeof avatarVariants>["size"]>;
|
|
7
|
+
type AvatarBadge = "member" | "super-connector" | "warning";
|
|
8
|
+
/** "Priya Natarajan" → "PN", "Priya" → "P", "" → "?". First and last word, so middle names don't crowd it. */
|
|
9
|
+
declare function initialsOf(name: string | null | undefined): string;
|
|
10
|
+
type AvatarProps = {
|
|
11
|
+
/** The person's name. Used for the initials, and read out when `standalone`. */
|
|
12
|
+
name: string | null | undefined;
|
|
13
|
+
/** Photo URL. Leave empty and the initials show. */
|
|
14
|
+
src?: string | null;
|
|
15
|
+
/** 16, 24, 32, 40, 48 or 64px. */
|
|
16
|
+
size?: AvatarSize;
|
|
17
|
+
/** A corner mark: Bridge member, Super Connector, or an account that needs attention. Not shown at `xs`. */
|
|
18
|
+
badge?: AvatarBadge | null;
|
|
19
|
+
/**
|
|
20
|
+
* Set when no name shows next to the avatar, so screen readers hear who it is. Most avatars sit
|
|
21
|
+
* beside the name, and only the badge (if any) is read, so the name isn't read twice.
|
|
22
|
+
*/
|
|
23
|
+
standalone?: boolean;
|
|
24
|
+
/** Layout only: `mt-0.5`. */
|
|
25
|
+
className?: string;
|
|
26
|
+
/** For AvatarGroup's stacking order. */
|
|
27
|
+
style?: CSSProperties;
|
|
28
|
+
};
|
|
29
|
+
declare function Avatar({ name, src, size, badge, standalone, className, style }: AvatarProps): import("react").JSX.Element;
|
|
30
|
+
type AvatarGroupProps = {
|
|
31
|
+
/** Everyone in the group, in order. The first ones show. */
|
|
32
|
+
people: {
|
|
33
|
+
name: string | null | undefined;
|
|
34
|
+
src?: string | null;
|
|
35
|
+
}[];
|
|
36
|
+
/** How many faces show before "+N". */
|
|
37
|
+
max?: number;
|
|
38
|
+
/** 16 to 64px. Stacks are usually `sm` (24px). */
|
|
39
|
+
size?: AvatarSize;
|
|
40
|
+
/**
|
|
41
|
+
* Set when the names are written next to the stack. Otherwise screen readers hear the names:
|
|
42
|
+
* "Leah Okonkwo, Priya Natarajan, Mei Lin Zhou and 4 others".
|
|
43
|
+
*/
|
|
44
|
+
decorative?: boolean;
|
|
45
|
+
/** Layout only. */
|
|
46
|
+
className?: string;
|
|
47
|
+
};
|
|
48
|
+
declare function AvatarGroup({ people, max, size, decorative, className }: AvatarGroupProps): import("react").JSX.Element;
|
|
49
|
+
export { Avatar, AvatarGroup, avatarVariants, initialsOf };
|
|
50
|
+
export type { AvatarBadge, AvatarGroupProps, AvatarProps, AvatarSize };
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Avatar as AvatarPrimitive } from "@base-ui/react/avatar";
|
|
4
|
+
import { cva } from "class-variance-authority";
|
|
5
|
+
import { cn } from "@brdgapp/ui/lib/utils";
|
|
6
|
+
// A person's photo in a circle, or their initials when there's no photo, with an optional corner badge.
|
|
7
|
+
// Started from shadcn's Base UI avatar, then fitted to the Bridge apps, which draw this by hand in over
|
|
8
|
+
// 100 places across 12 repos. Base UI shows the initials while the photo loads and keeps them if the
|
|
9
|
+
// photo fails, so a broken URL never shows a broken image.
|
|
10
|
+
//
|
|
11
|
+
// Decisions (2026-11-03, after checking all 12 app repos):
|
|
12
|
+
// - Initials on light blue (`--info-bg`), letters in `--info` (not `--primary`: 3.3:1 in dark).
|
|
13
|
+
// - Sizes 16, 24, 32, 40, 48, 64: stacks and chips 16–24, lists 32, rows 40, cards 48, profile headers 64.
|
|
14
|
+
// - Badges, as in Bridge OS contact-card.tsx and app-cc avatar-status-badge.tsx: a blue tick for a Bridge
|
|
15
|
+
// member, a purple award for a Super Connector (replaces the tick), an amber "!" when the account is
|
|
16
|
+
// deactivated or its emails fail. The warning amber is darker than app-cc's #E19500, which gave the
|
|
17
|
+
// white mark 2.5:1; #B87A00 gives 3.6:1.
|
|
18
|
+
// - AvatarGroup: 3 faces then "+N", overlapping a little less than the apps do, a ring between faces (app-directories, together,
|
|
19
|
+
// app-portfolio, raise, bridge-jobs).
|
|
20
|
+
const avatarVariants = cva("relative inline-flex shrink-0 rounded-full bg-[var(--info-bg)] align-middle select-none", {
|
|
21
|
+
variants: {
|
|
22
|
+
size: {
|
|
23
|
+
xs: "size-4 text-[8px]",
|
|
24
|
+
sm: "size-6 text-[10px]",
|
|
25
|
+
md: "size-8 text-[12px]",
|
|
26
|
+
lg: "size-10 text-[14px]",
|
|
27
|
+
xl: "size-12 text-[16px]",
|
|
28
|
+
"2xl": "size-16 text-[20px]",
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
defaultVariants: { size: "md" },
|
|
32
|
+
});
|
|
33
|
+
const BADGE_LABEL = {
|
|
34
|
+
member: "Bridge member",
|
|
35
|
+
"super-connector": "Super Connector",
|
|
36
|
+
warning: "Account needs attention",
|
|
37
|
+
};
|
|
38
|
+
// Badge diameter for each avatar size. None at 16px: too small to read.
|
|
39
|
+
const BADGE_SIZE = {
|
|
40
|
+
xs: null,
|
|
41
|
+
sm: "size-2.5 ring-1",
|
|
42
|
+
md: "size-3 ring-[1.5px]",
|
|
43
|
+
lg: "size-3.5 ring-2",
|
|
44
|
+
xl: "size-[18px] ring-2",
|
|
45
|
+
"2xl": "size-5 ring-2",
|
|
46
|
+
};
|
|
47
|
+
/** "Priya Natarajan" → "PN", "Priya" → "P", "" → "?". First and last word, so middle names don't crowd it. */
|
|
48
|
+
function initialsOf(name) {
|
|
49
|
+
const words = (name ?? "").trim().split(/\s+/).filter(Boolean);
|
|
50
|
+
const first = words[0]?.[0] ?? "";
|
|
51
|
+
const last = words.length > 1 ? (words[words.length - 1]?.[0] ?? "") : "";
|
|
52
|
+
return (first + last).toUpperCase() || "?";
|
|
53
|
+
}
|
|
54
|
+
function BadgeMark({ badge }) {
|
|
55
|
+
if (badge === "member") {
|
|
56
|
+
return (_jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "4", strokeLinecap: "round", strokeLinejoin: "round", className: "size-[60%]", children: _jsx("polyline", { points: "20 6 9 17 4 12" }) }));
|
|
57
|
+
}
|
|
58
|
+
if (badge === "super-connector") {
|
|
59
|
+
// The award glyph from the Arch Design System, via Bridge OS super-connector-badge.tsx.
|
|
60
|
+
return (_jsx("svg", { viewBox: "0 0 8.24323 14.1285", fill: "currentColor", className: "h-[60%] w-auto", children: _jsx("path", { fillRule: "evenodd", clipRule: "evenodd", d: "M0.136812 0.00229937H3.11564L5.79901 4.88271C5.85649 4.98618 5.94042 5.06781 6.03584 5.11954H2.03034L0.136812 0V0.00229937ZM4.12162 5.88523C6.39799 5.88523 8.24323 7.73047 8.24323 10.0068C8.24323 12.2832 6.39799 14.1285 4.12162 14.1285C1.84524 14.1273 0 12.2821 0 10.0057C0 7.72932 1.84524 5.88523 4.12162 5.88523ZM4.12162 7.627L4.81028 9.30784L6.62333 9.44465L5.23566 10.6219L5.66679 12.3855L4.12162 11.4279L2.57644 12.3855L3.00757 10.6219L1.61875 9.4435L3.43181 9.30669L4.12162 7.627ZM4.48722 0.00229937H7.97306L6.57044 3.79396L4.48722 0.00229937Z" }) }));
|
|
61
|
+
}
|
|
62
|
+
return (_jsxs("svg", { viewBox: "0 0 16 16", fill: "currentColor", className: "size-[70%]", children: [_jsx("path", { d: "M8 3.2c-.59 0-1.05.5-1.02 1.08l.25 4a.77.77 0 0 0 1.53 0l.25-4A1.02 1.02 0 0 0 8 3.2Z" }), _jsx("circle", { cx: "8", cy: "11.6", r: "0.95" })] }));
|
|
63
|
+
}
|
|
64
|
+
const BADGE_FILL = {
|
|
65
|
+
member: "bg-[var(--primary-solid)]",
|
|
66
|
+
"super-connector": "bg-linear-to-l from-[var(--super-connector-from)] to-[var(--super-connector-to)]",
|
|
67
|
+
warning: "bg-[var(--badge-warning)]",
|
|
68
|
+
};
|
|
69
|
+
function Avatar({ name, src, size = "md", badge, standalone = false, className, style }) {
|
|
70
|
+
const badgeSize = badge ? BADGE_SIZE[size] : null;
|
|
71
|
+
const label = badge ? BADGE_LABEL[badge] : null;
|
|
72
|
+
return (_jsxs(AvatarPrimitive.Root, { "data-slot": "avatar", "data-size": size, className: cn(avatarVariants({ size }), className), style: style, ...(standalone ? { role: "img", "aria-label": [name || "Unknown person", label].filter(Boolean).join(", ") } : {}), children: [_jsxs("span", { className: "block size-full overflow-hidden rounded-full", "aria-hidden": "true", children: [src ? _jsx(AvatarPrimitive.Image, { "data-slot": "avatar-image", src: src, alt: "", className: "size-full object-cover" }) : null, _jsx(AvatarPrimitive.Fallback, { "data-slot": "avatar-fallback", className: "flex size-full items-center justify-center leading-none font-bold text-[var(--info)]", children: initialsOf(name) })] }), badge && badgeSize ? (_jsx("span", { "data-slot": "avatar-badge", "data-badge": badge, "aria-hidden": "true", className: cn("absolute -right-0.5 -bottom-0.5 flex items-center justify-center rounded-full text-white ring-[var(--surface-primary)]", BADGE_FILL[badge], badgeSize), children: _jsx(BadgeMark, { badge: badge }) })) : null, badge && !standalone ? _jsx("span", { className: "sr-only", children: label }) : null] }));
|
|
73
|
+
}
|
|
74
|
+
// How far each face tucks under the one before it: a quarter of the face less the ring, so two-letter
|
|
75
|
+
// initials stay readable (the apps' 6px at 24px hid the first letter).
|
|
76
|
+
const OVERLAP = {
|
|
77
|
+
xs: "-ml-0.5",
|
|
78
|
+
sm: "-ml-1",
|
|
79
|
+
md: "-ml-1.5",
|
|
80
|
+
lg: "-ml-2",
|
|
81
|
+
xl: "-ml-2.5",
|
|
82
|
+
"2xl": "-ml-3",
|
|
83
|
+
};
|
|
84
|
+
function AvatarGroup({ people, max = 3, size = "sm", decorative = false, className }) {
|
|
85
|
+
const shown = people.slice(0, max);
|
|
86
|
+
const extra = people.length - shown.length;
|
|
87
|
+
const names = shown.map((p) => p.name || "Unknown person");
|
|
88
|
+
const label = extra > 0 ? `${names.join(", ")} and ${extra} ${extra === 1 ? "other" : "others"}` : names.join(", ");
|
|
89
|
+
return (_jsxs("span", { "data-slot": "avatar-group", className: cn("inline-flex items-center align-middle", className), ...(decorative ? { "aria-hidden": true } : { role: "img", "aria-label": label }), children: [shown.map((p, i) => (_jsx(Avatar, { name: p.name, src: p.src, size: size, className: cn("ring-2 ring-[var(--surface-primary)]", i > 0 && OVERLAP[size]), style: { zIndex: shown.length - i } }, `${p.name}-${i}`))), extra > 0 ? (_jsxs("span", { "data-slot": "avatar-group-count", className: cn(avatarVariants({ size }), OVERLAP[size], "items-center justify-center bg-[var(--surface-tertiary)] leading-none font-semibold text-[var(--text-secondary)] ring-2 ring-[var(--surface-primary)]"), children: ["+", extra] })) : null] }));
|
|
90
|
+
}
|
|
91
|
+
export { Avatar, AvatarGroup, avatarVariants, initialsOf };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type * as React from "react";
|
|
2
|
+
import { type VariantProps } from "class-variance-authority";
|
|
3
|
+
declare const badgeVariants: (props?: ({
|
|
4
|
+
style?: "outline" | "alum" | "solid-red" | "solid-dark" | null | undefined;
|
|
5
|
+
} & import("class-variance-authority/types").ClassProp) | undefined) => string;
|
|
6
|
+
type BadgeProps = VariantProps<typeof badgeVariants> & {
|
|
7
|
+
className?: string;
|
|
8
|
+
children: React.ReactNode;
|
|
9
|
+
};
|
|
10
|
+
declare function Badge({ style, className, children }: BadgeProps): React.JSX.Element;
|
|
11
|
+
export { Badge, badgeVariants };
|
|
12
|
+
export type { BadgeProps };
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { cva } from "class-variance-authority";
|
|
3
|
+
import { cn } from "@brdgapp/ui/lib/utils";
|
|
4
|
+
// The "Solid label" sticker from the Arch Design System: 8px, bold, uppercase. Ported from Bridge OS
|
|
5
|
+
// src/components/ui/badge.tsx. Rounded 4px, never a pill. For soft status and category labels use Tag.
|
|
6
|
+
// Two fixes over Bridge OS: solid-red fills with --danger-solid (white on --error fails AA at 4.41:1), and
|
|
7
|
+
// solid-dark's text uses --surface-primary so it stays readable in dark mode (white on light grey didn't).
|
|
8
|
+
const badgeVariants = cva("inline-flex items-center rounded-[4px] px-[7px] py-1 text-[8px] leading-[8px] font-bold tracking-[1px] uppercase", {
|
|
9
|
+
variants: {
|
|
10
|
+
// Named `style` to match Bridge OS's API. It is not React's style prop: Badge takes no CSS styles.
|
|
11
|
+
style: {
|
|
12
|
+
outline: "border border-[var(--text-disabled)] text-[var(--text-muted)]",
|
|
13
|
+
alum: "border border-[var(--honey)] text-[var(--honey)]",
|
|
14
|
+
"solid-red": "bg-[var(--danger-solid)] text-white",
|
|
15
|
+
"solid-dark": "bg-[var(--text-secondary)] text-[var(--surface-primary)]",
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
defaultVariants: { style: "outline" },
|
|
19
|
+
});
|
|
20
|
+
function Badge({ style, className, children }) {
|
|
21
|
+
return (_jsx("span", { "data-slot": "badge", className: cn(badgeVariants({ style }), className), children: children }));
|
|
22
|
+
}
|
|
23
|
+
export { Badge, badgeVariants };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type * as React from "react";
|
|
2
|
+
type BottomSheetProps = {
|
|
3
|
+
open: boolean;
|
|
4
|
+
onClose: () => void;
|
|
5
|
+
/** What's being chosen: "Degree", "Sort by". */
|
|
6
|
+
title: React.ReactNode;
|
|
7
|
+
children: React.ReactNode;
|
|
8
|
+
};
|
|
9
|
+
declare function BottomSheet({ open, onClose, title, children }: BottomSheetProps): React.JSX.Element;
|
|
10
|
+
/** One choice in a sheet: a 44px row with a tick when chosen. Wrap a set in `role="radiogroup"`. */
|
|
11
|
+
declare function BottomSheetOption({ selected, onSelect, children }: {
|
|
12
|
+
selected?: boolean;
|
|
13
|
+
onSelect: () => void;
|
|
14
|
+
children: React.ReactNode;
|
|
15
|
+
}): React.JSX.Element;
|
|
16
|
+
export { BottomSheet, BottomSheetOption };
|
|
17
|
+
export type { BottomSheetProps };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Dialog as DialogPrimitive } from "@base-ui/react/dialog";
|
|
4
|
+
import { cn } from "@brdgapp/ui/lib/utils";
|
|
5
|
+
function BottomSheet({ open, onClose, title, children }) {
|
|
6
|
+
return (_jsx(DialogPrimitive.Root, { open: open, onOpenChange: (next) => !next && onClose(), children: _jsxs(DialogPrimitive.Portal, { children: [_jsx(DialogPrimitive.Backdrop, { className: "fixed inset-0 z-[80] bg-black/30 transition-opacity duration-300 ease-out data-[ending-style]:duration-200 data-[starting-style]:opacity-0 data-[ending-style]:opacity-0" }), _jsxs(DialogPrimitive.Popup, { "data-slot": "bottom-sheet", className: cn("fixed inset-x-0 bottom-0 z-[80] flex max-h-[85dvh] flex-col rounded-t-2xl border-t border-[var(--border-default)] bg-[var(--surface-elevated)] shadow-[var(--shadow-dialog)] outline-none", "pb-[max(12px,env(safe-area-inset-bottom))]", "transition-transform duration-300 ease-out data-[ending-style]:duration-200 data-[starting-style]:translate-y-full data-[ending-style]:translate-y-full motion-reduce:transition-none"), children: [_jsx("div", { "aria-hidden": "true", className: "mx-auto mt-2 h-1 w-10 shrink-0 rounded-full bg-[var(--border-hover)]" }), _jsx(DialogPrimitive.Title, { className: "shrink-0 px-5 pt-3 pb-2 text-16 font-bold tracking-tight text-[var(--text-primary)]", children: title }), _jsx("div", { "data-slot": "bottom-sheet-body", className: "min-h-0 flex-1 overflow-y-auto overscroll-contain", children: children })] })] }) }));
|
|
7
|
+
}
|
|
8
|
+
/** One choice in a sheet: a 44px row with a tick when chosen. Wrap a set in `role="radiogroup"`. */
|
|
9
|
+
function BottomSheetOption({ selected, onSelect, children }) {
|
|
10
|
+
return (_jsxs("button", { type: "button", role: "radio", "aria-checked": selected, onClick: onSelect, "data-slot": "bottom-sheet-option", className: cn("flex min-h-11 w-full items-center justify-between gap-3 px-5 text-left text-14 outline-none transition-colors", "active:bg-[var(--surface-tertiary)] focus-visible:bg-[var(--surface-secondary)]", selected ? "font-semibold text-[var(--text-primary)]" : "text-[var(--text-body)]"), children: [_jsx("span", { className: "truncate", children: children }), selected ? (_jsx("svg", { "aria-hidden": "true", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.5", strokeLinecap: "round", strokeLinejoin: "round", className: "size-4 shrink-0 text-[var(--primary)]", children: _jsx("polyline", { points: "20 6 9 17 4 12" }) })) : null] }));
|
|
11
|
+
}
|
|
12
|
+
export { BottomSheet, BottomSheetOption };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export type Brand = "linkedin" | "google" | "microsoft" | "apple" | "whatsapp" | "telegram" | "chrome";
|
|
2
|
+
declare const NAME: Record<Brand, string>;
|
|
3
|
+
type BrandMarkProps = {
|
|
4
|
+
brand: Brand;
|
|
5
|
+
/**
|
|
6
|
+
* Say the company's name to screen readers. Leave it off when its name is already written next to it
|
|
7
|
+
* ("Sign in with Google"): the mark is then hidden from them. Set it when the mark stands alone.
|
|
8
|
+
*/
|
|
9
|
+
label?: boolean;
|
|
10
|
+
/**
|
|
11
|
+
* One color that follows the text, for a quiet mark in a row of links. LinkedIn, WhatsApp and Telegram
|
|
12
|
+
* only; Google, Microsoft and Chrome stay in color, and Apple is always one color.
|
|
13
|
+
*/
|
|
14
|
+
mono?: boolean;
|
|
15
|
+
/** Size and layout only: `size-3.5`, `shrink-0`. 16px by default, like an icon. */
|
|
16
|
+
className?: string;
|
|
17
|
+
};
|
|
18
|
+
/** A company's logo at icon size, in its own colors. Not an icon: Lucide has none of these. */
|
|
19
|
+
declare function BrandMark({ brand, label, mono, className }: BrandMarkProps): import("react").JSX.Element;
|
|
20
|
+
export { BrandMark, NAME as BRAND_NAMES };
|