@brdgapp/ui 0.1.2 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. package/CHANGELOG.md +83 -0
  2. package/README.md +37 -201
  3. package/components/action-menu.d.ts +65 -0
  4. package/components/action-menu.js +48 -0
  5. package/components/avatar.d.ts +50 -0
  6. package/components/avatar.js +91 -0
  7. package/components/badge.d.ts +12 -0
  8. package/components/badge.js +23 -0
  9. package/components/bottom-sheet.d.ts +17 -0
  10. package/components/bottom-sheet.js +12 -0
  11. package/components/brand-mark.d.ts +20 -0
  12. package/components/brand-mark.js +44 -0
  13. package/components/button-group.d.ts +25 -0
  14. package/components/button-group.js +53 -0
  15. package/components/button.d.ts +33 -0
  16. package/components/button.js +82 -0
  17. package/components/card.d.ts +78 -0
  18. package/components/card.js +75 -0
  19. package/components/checkbox.d.ts +22 -0
  20. package/components/checkbox.js +35 -0
  21. package/components/collapsible.d.ts +29 -0
  22. package/components/collapsible.js +38 -0
  23. package/components/column-filter-menu.d.ts +15 -0
  24. package/components/column-filter-menu.js +30 -0
  25. package/components/column-sort-button.d.ts +18 -0
  26. package/components/column-sort-button.js +14 -0
  27. package/components/confirm-dialog.d.ts +20 -0
  28. package/components/confirm-dialog.js +17 -0
  29. package/components/copy-field.d.ts +17 -0
  30. package/components/copy-field.js +36 -0
  31. package/components/data-state.d.ts +44 -0
  32. package/components/data-state.js +25 -0
  33. package/components/data-table.d.ts +65 -0
  34. package/components/data-table.js +84 -0
  35. package/components/dialog.d.ts +19 -0
  36. package/components/dialog.js +10 -0
  37. package/components/drawer.d.ts +21 -0
  38. package/components/drawer.js +17 -0
  39. package/components/expandable-bio.d.ts +8 -0
  40. package/components/expandable-bio.js +26 -0
  41. package/components/field.d.ts +29 -0
  42. package/components/field.js +27 -0
  43. package/components/filter-chip.d.ts +11 -0
  44. package/components/filter-chip.js +8 -0
  45. package/components/filter-pill.d.ts +34 -0
  46. package/components/filter-pill.js +35 -0
  47. package/components/hover-card.d.ts +21 -0
  48. package/components/hover-card.js +20 -0
  49. package/components/icon-button.d.ts +16 -0
  50. package/components/icon-button.js +32 -0
  51. package/components/illustration.d.ts +33 -0
  52. package/components/illustration.js +70 -0
  53. package/components/info-hint.d.ts +29 -0
  54. package/components/info-hint.js +42 -0
  55. package/components/input.d.ts +9 -0
  56. package/components/input.js +15 -0
  57. package/components/network-graph.d.ts +48 -0
  58. package/components/network-graph.js +37 -0
  59. package/components/page-banner.d.ts +20 -0
  60. package/components/page-banner.js +29 -0
  61. package/components/scene.d.ts +12 -0
  62. package/components/scene.js +7 -0
  63. package/components/search-input.d.ts +8 -0
  64. package/components/search-input.js +14 -0
  65. package/components/segmented-control.d.ts +18 -0
  66. package/components/segmented-control.js +14 -0
  67. package/components/skeleton.d.ts +12 -0
  68. package/components/skeleton.js +21 -0
  69. package/components/skip-link.d.ts +8 -0
  70. package/components/skip-link.js +7 -0
  71. package/components/stat-box.d.ts +23 -0
  72. package/components/stat-box.js +29 -0
  73. package/components/switch.d.ts +13 -0
  74. package/components/switch.js +8 -0
  75. package/components/tabs.d.ts +6 -0
  76. package/components/tabs.js +25 -0
  77. package/components/tag-input.d.ts +15 -0
  78. package/components/tag-input.js +40 -0
  79. package/components/tag.d.ts +27 -0
  80. package/components/tag.js +35 -0
  81. package/components/textarea.d.ts +12 -0
  82. package/components/textarea.js +78 -0
  83. package/components/theme-switch.d.ts +6 -0
  84. package/components/theme-switch.js +16 -0
  85. package/components/toast.d.ts +25 -0
  86. package/components/toast.js +44 -0
  87. package/hooks/use-is-mobile.d.ts +2 -0
  88. package/hooks/use-is-mobile.js +16 -0
  89. package/hooks/use-mock-query.d.ts +23 -0
  90. package/hooks/use-mock-query.js +36 -0
  91. package/hooks/use-theme.d.ts +11 -0
  92. package/hooks/use-theme.js +34 -0
  93. package/lib/demo-controls.d.ts +24 -0
  94. package/lib/demo-controls.js +24 -0
  95. package/lib/format.d.ts +17 -0
  96. package/lib/format.js +55 -0
  97. package/lib/layout.d.ts +19 -0
  98. package/lib/layout.js +24 -0
  99. package/lib/motion.d.ts +16 -0
  100. package/lib/motion.js +18 -0
  101. package/lib/theme.d.ts +21 -0
  102. package/lib/theme.js +63 -0
  103. package/lib/utils.d.ts +22 -0
  104. package/lib/utils.js +27 -0
  105. package/package.json +32 -16
  106. package/styles/globals.css +595 -0
  107. package/src/index.ts +0 -18
  108. package/src/network-people-helper/README.md +0 -130
  109. package/src/network-people-helper/api.ts +0 -174
  110. package/src/network-people-helper/contact-avatar.tsx +0 -63
  111. package/src/network-people-helper/domain.ts +0 -14
  112. package/src/network-people-helper/icons.tsx +0 -130
  113. package/src/network-people-helper/index.ts +0 -6
  114. package/src/network-people-helper/network-people-helper.tsx +0 -397
  115. package/src/network-people-helper/network-people-nudge.tsx +0 -98
  116. package/src/network-people-helper/open-helper.ts +0 -22
  117. package/src/network-people-helper/person-menu.tsx +0 -211
  118. package/src/network-people-helper/styles.css +0 -848
  119. package/src/network-people-helper/types.ts +0 -23
  120. package/src/network-people-helper/use-network-people-list.ts +0 -122
  121. package/src/network-people-helper/use-network-preview.ts +0 -43
  122. package/src/provider.tsx +0 -59
  123. package/src/styles.css +0 -1
@@ -0,0 +1,34 @@
1
+ "use client";
2
+ import { useCallback, useSyncExternalStore } from "react";
3
+ import { applyTheme, DARK_QUERY, readPreference, resolveTheme, writePreference } from "@brdgapp/ui/lib/theme";
4
+ // One small store for the theme, shared by every useTheme() on the page. No provider to wrap the app in.
5
+ // It follows the device while the choice is System, and picks up a choice made in another Bridge app when
6
+ // people come back to this tab.
7
+ const CHANGE = "bridge-theme-change";
8
+ let listening = false;
9
+ function sync() {
10
+ applyTheme(resolveTheme(readPreference()));
11
+ window.dispatchEvent(new Event(CHANGE));
12
+ }
13
+ function subscribe(onChange) {
14
+ if (!listening) {
15
+ listening = true;
16
+ window.matchMedia(DARK_QUERY).addEventListener("change", sync);
17
+ window.addEventListener("focus", sync);
18
+ document.addEventListener("visibilitychange", () => document.visibilityState === "visible" && sync());
19
+ }
20
+ window.addEventListener(CHANGE, onChange);
21
+ return () => window.removeEventListener(CHANGE, onChange);
22
+ }
23
+ const snapshot = () => `${readPreference()}:${resolveTheme(readPreference())}`;
24
+ // Before the page has loaded there's no saved choice to read: report System, drawn light.
25
+ const serverSnapshot = () => "system:light";
26
+ /** The theme choice, what's drawn, and a way to change it. Needs `themeScript` in the root layout's <head>. */
27
+ export function useTheme() {
28
+ const [theme, resolvedTheme] = useSyncExternalStore(subscribe, snapshot, serverSnapshot).split(":");
29
+ const setTheme = useCallback((next) => {
30
+ writePreference(next);
31
+ sync();
32
+ }, []);
33
+ return { theme, resolvedTheme, setTheme };
34
+ }
@@ -0,0 +1,24 @@
1
+ import { type ReactNode } from "react";
2
+ /**
3
+ * Global settings for handbook demos: how slow fake data is, whether it fails, and a "replay" counter
4
+ * that makes every demo load again. Read by `useMockQuery`. Demo-only — product code never uses it.
5
+ */
6
+ type DemoControls = {
7
+ /** Fake network delay in milliseconds. */
8
+ latency: number;
9
+ /** When true, every mock query fails, so error states can be seen. */
10
+ fail: boolean;
11
+ /** Bumped by `replay()`; every mock query reloads when it changes. */
12
+ replayKey: number;
13
+ setLatency: (ms: number) => void;
14
+ setFail: (fail: boolean) => void;
15
+ replay: () => void;
16
+ };
17
+ declare const DEFAULT_LATENCY = 900;
18
+ declare function DemoControlsProvider({ children }: {
19
+ children: ReactNode;
20
+ }): import("react").FunctionComponentElement<import("react").ProviderProps<DemoControls | null>>;
21
+ /** Outside a provider, demos still work with the defaults (no failure, default delay). */
22
+ declare function useDemoControls(): DemoControls;
23
+ export { DemoControlsProvider, useDemoControls, DEFAULT_LATENCY };
24
+ export type { DemoControls };
@@ -0,0 +1,24 @@
1
+ "use client";
2
+ import { createContext, createElement, useCallback, useContext, useMemo, useState } from "react";
3
+ const DEFAULT_LATENCY = 900;
4
+ const DemoControlsContext = createContext(null);
5
+ function DemoControlsProvider({ children }) {
6
+ const [latency, setLatency] = useState(DEFAULT_LATENCY);
7
+ const [fail, setFail] = useState(false);
8
+ const [replayKey, setReplayKey] = useState(0);
9
+ const replay = useCallback(() => setReplayKey((k) => k + 1), []);
10
+ const value = useMemo(() => ({ latency, fail, replayKey, setLatency, setFail, replay }), [latency, fail, replayKey, replay]);
11
+ return createElement(DemoControlsContext.Provider, { value }, children);
12
+ }
13
+ /** Outside a provider, demos still work with the defaults (no failure, default delay). */
14
+ function useDemoControls() {
15
+ return (useContext(DemoControlsContext) ?? {
16
+ latency: DEFAULT_LATENCY,
17
+ fail: false,
18
+ replayKey: 0,
19
+ setLatency: () => { },
20
+ setFail: () => { },
21
+ replay: () => { },
22
+ });
23
+ }
24
+ export { DemoControlsProvider, useDemoControls, DEFAULT_LATENCY };
@@ -0,0 +1,17 @@
1
+ /**
2
+ * A count. In full with a comma under 10,000 (1,284); compact from 10,000 with a capital K or M and at
3
+ * most one decimal, never ".0" (12K, 12.5K, 1.3M).
4
+ */
5
+ export declare function formatCount(n: number): string;
6
+ /** Money in whole dollars, the same steps as counts: $950, $9,500, $270K, $1.2M; a minus before the symbol. */
7
+ export declare function formatMoney(n: number): string;
8
+ /** A share: no space, at most one decimal, never ".0" (92.4%, 2%). */
9
+ export declare function formatPercent(n: number): string;
10
+ /**
11
+ * A date as Bridge writes it: relative under 7 days (Today, Yesterday, 4 days ago), then the date
12
+ * (Jul 20), with the year when it isn't this year (Jul 20, 2025). Future dates are always absolute.
13
+ * `now` is for tests and fixed examples; it defaults to the current time.
14
+ */
15
+ export declare function formatDate(date: Date | string, now?: Date): string;
16
+ /** A time of day: 12-hour with minutes, a space, then AM or PM (9:40 AM). */
17
+ export declare function formatTime(date: Date | string): string;
package/lib/format.js ADDED
@@ -0,0 +1,55 @@
1
+ // Bridge's writing rules for numbers, money, dates and times, as functions, so every app formats them the
2
+ // same way (handbook → Brand → Writing). American English (decision 2026-11-03).
3
+ const COMPACT_FROM = 10_000;
4
+ /** Strips a trailing ".0" so 12.0 reads 12. */
5
+ const oneDecimal = (n) => {
6
+ const s = n.toFixed(1);
7
+ return s.endsWith(".0") ? s.slice(0, -2) : s;
8
+ };
9
+ /**
10
+ * A count. In full with a comma under 10,000 (1,284); compact from 10,000 with a capital K or M and at
11
+ * most one decimal, never ".0" (12K, 12.5K, 1.3M).
12
+ */
13
+ export function formatCount(n) {
14
+ const sign = n < 0 ? "-" : "";
15
+ const a = Math.abs(n);
16
+ // From 999,950 the K form would round to "1000K", so it becomes 1M.
17
+ if (a >= 999_950)
18
+ return `${sign}${oneDecimal(a / 1_000_000)}M`;
19
+ if (a >= COMPACT_FROM)
20
+ return `${sign}${oneDecimal(a / 1_000)}K`;
21
+ return `${sign}${Math.round(a).toLocaleString("en-US")}`;
22
+ }
23
+ /** Money in whole dollars, the same steps as counts: $950, $9,500, $270K, $1.2M; a minus before the symbol. */
24
+ export function formatMoney(n) {
25
+ const body = formatCount(Math.abs(n));
26
+ return `${n < 0 ? "-" : ""}$${body}`;
27
+ }
28
+ /** A share: no space, at most one decimal, never ".0" (92.4%, 2%). */
29
+ export function formatPercent(n) {
30
+ return `${oneDecimal(n)}%`;
31
+ }
32
+ const DAY = 86_400_000;
33
+ const startOfDay = (d) => Date.UTC(d.getFullYear(), d.getMonth(), d.getDate());
34
+ /**
35
+ * A date as Bridge writes it: relative under 7 days (Today, Yesterday, 4 days ago), then the date
36
+ * (Jul 20), with the year when it isn't this year (Jul 20, 2025). Future dates are always absolute.
37
+ * `now` is for tests and fixed examples; it defaults to the current time.
38
+ */
39
+ export function formatDate(date, now = new Date()) {
40
+ const d = typeof date === "string" ? new Date(date) : date;
41
+ const days = Math.round((startOfDay(now) - startOfDay(d)) / DAY);
42
+ if (days === 0)
43
+ return "Today";
44
+ if (days === 1)
45
+ return "Yesterday";
46
+ if (days > 1 && days < 7)
47
+ return `${days} days ago`;
48
+ const sameYear = d.getFullYear() === now.getFullYear();
49
+ return d.toLocaleDateString("en-US", { month: "short", day: "numeric", year: sameYear ? undefined : "numeric" });
50
+ }
51
+ /** A time of day: 12-hour with minutes, a space, then AM or PM (9:40 AM). */
52
+ export function formatTime(date) {
53
+ const d = typeof date === "string" ? new Date(date) : date;
54
+ return d.toLocaleTimeString("en-US", { hour: "numeric", minute: "2-digit" });
55
+ }
@@ -0,0 +1,19 @@
1
+ /** How wide a page's content gets. Pick by the page's job. */
2
+ export declare const PAGE_WIDTH: {
3
+ /** Tables and record pages with a side rail: 1600px. */
4
+ readonly wide: "max-w-[1600px]";
5
+ /** Card dashboards, grids and feeds: 1280px. */
6
+ readonly standard: "max-w-7xl";
7
+ /** A single column of reading: 800px. */
8
+ readonly read: "max-w-[800px]";
9
+ /** Forms and settings: 720px. */
10
+ readonly form: "max-w-[720px]";
11
+ /** A centred card outside the app, like sign-in: 448px. */
12
+ readonly narrow: "max-w-md";
13
+ };
14
+ /** The page's side edges: 16px on phones, 24px from 640px, 32px from 1024px. */
15
+ export declare const PAGE_EDGES = "px-4 sm:px-6 lg:px-8";
16
+ /** A page's frame: centred, its width, its edges, 24px from the top bar on phones and 32px from 640px. */
17
+ export declare function pageFrame(width: keyof typeof PAGE_WIDTH): string;
18
+ /** The side rail beside a record page's main column, from 1024px: 340px. */
19
+ export declare const RAIL = "lg:grid-cols-[minmax(0,1fr)_340px]";
package/lib/layout.js ADDED
@@ -0,0 +1,24 @@
1
+ // Page widths and edges, shared by every app (decisions 2026-11-03, from a survey of all 12 app repos).
2
+ // Values from Bridge OS src/lib/layout/widths.ts, plus a narrow width for sign-in cards (apps used six
3
+ // values between 360 and 560px). Classes, not numbers, so Tailwind generates them from the package.
4
+ /** How wide a page's content gets. Pick by the page's job. */
5
+ export const PAGE_WIDTH = {
6
+ /** Tables and record pages with a side rail: 1600px. */
7
+ wide: "max-w-[1600px]",
8
+ /** Card dashboards, grids and feeds: 1280px. */
9
+ standard: "max-w-7xl",
10
+ /** A single column of reading: 800px. */
11
+ read: "max-w-[800px]",
12
+ /** Forms and settings: 720px. */
13
+ form: "max-w-[720px]",
14
+ /** A centred card outside the app, like sign-in: 448px. */
15
+ narrow: "max-w-md",
16
+ };
17
+ /** The page's side edges: 16px on phones, 24px from 640px, 32px from 1024px. */
18
+ export const PAGE_EDGES = "px-4 sm:px-6 lg:px-8";
19
+ /** A page's frame: centred, its width, its edges, 24px from the top bar on phones and 32px from 640px. */
20
+ export function pageFrame(width) {
21
+ return `mx-auto w-full ${PAGE_WIDTH[width]} ${PAGE_EDGES} pt-6 pb-12 sm:pt-8`;
22
+ }
23
+ /** The side rail beside a record page's main column, from 1024px: 340px. */
24
+ export const RAIL = "lg:grid-cols-[minmax(0,1fr)_340px]";
@@ -0,0 +1,16 @@
1
+ /** Seconds, for Framer Motion's `duration`. */
2
+ export declare const DURATION: {
3
+ /** 150ms: hover, color, press. */
4
+ readonly quick: 0.15;
5
+ /** 200ms: menus, sections, toasts: small things opening. */
6
+ readonly base: 0.2;
7
+ /** 300ms: drawers, sheets, dialogs: big things arriving. */
8
+ readonly slow: 0.3;
9
+ };
10
+ /** The one curve: fast out of the gate, gentle landing. Same as the package's `ease-out`. */
11
+ export declare const EASE: readonly [0.23, 1, 0.32, 1];
12
+ /** A ready-made transition for Framer Motion: `transition={transition("base")}`. */
13
+ export declare function transition(speed?: keyof typeof DURATION): {
14
+ duration: 0.15 | 0.2 | 0.3;
15
+ ease: readonly [0.23, 1, 0.32, 1];
16
+ };
package/lib/motion.js ADDED
@@ -0,0 +1,18 @@
1
+ // Bridge's motion, for JavaScript animations (Framer Motion and the like). CSS uses the same values:
2
+ // `duration-150` / `duration-200` / `duration-300` and `ease-out`, which the package sets to this curve.
3
+ // Decision 2026-11-03: three speeds and one curve; the package itself stays CSS-only.
4
+ /** Seconds, for Framer Motion's `duration`. */
5
+ export const DURATION = {
6
+ /** 150ms: hover, color, press. */
7
+ quick: 0.15,
8
+ /** 200ms: menus, sections, toasts: small things opening. */
9
+ base: 0.2,
10
+ /** 300ms: drawers, sheets, dialogs: big things arriving. */
11
+ slow: 0.3,
12
+ };
13
+ /** The one curve: fast out of the gate, gentle landing. Same as the package's `ease-out`. */
14
+ export const EASE = [0.23, 1, 0.32, 1];
15
+ /** A ready-made transition for Framer Motion: `transition={transition("base")}`. */
16
+ export function transition(speed = "base") {
17
+ return { duration: DURATION[speed], ease: EASE };
18
+ }
package/lib/theme.d.ts ADDED
@@ -0,0 +1,21 @@
1
+ export type ThemePreference = "light" | "dark" | "system";
2
+ export type ResolvedTheme = "light" | "dark";
3
+ export declare const THEME_COOKIE = "bridge-theme";
4
+ declare const DARK_QUERY = "(prefers-color-scheme: dark)";
5
+ /** The saved choice: the shared cookie, then Bridge OS's old per-app key, then System. */
6
+ export declare function readPreference(): ThemePreference;
7
+ /** Saves the choice for every Bridge app: on brdg.app it's shared by all subdomains; elsewhere (localhost, previews) by this site. */
8
+ export declare function writePreference(preference: ThemePreference): void;
9
+ export declare function systemTheme(): ResolvedTheme;
10
+ export declare function resolveTheme(preference: ThemePreference): ResolvedTheme;
11
+ /**
12
+ * Puts the theme on <html>. Transitions are switched off for one frame while it changes, so every
13
+ * `transition-colors` on the page doesn't fade at once.
14
+ */
15
+ export declare function applyTheme(theme: ResolvedTheme): void;
16
+ /**
17
+ * Runs in <head> before the page paints, so someone in dark never sees a white flash. The same steps as
18
+ * readPreference and resolveTheme, written out because it can't import anything.
19
+ */
20
+ export declare const themeScript = "(function(){try{var m=document.cookie.match(/(?:^|; )bridge-theme=(light|dark|system)/),p=m?m[1]:null;if(!p){try{p=localStorage.getItem(\"bridge-theme\")}catch(e){}}if(p!==\"light\"&&p!==\"dark\")p=\"system\";var t=p===\"dark\"||(p===\"system\"&&matchMedia(\"(prefers-color-scheme: dark)\").matches)?\"dark\":\"light\",r=document.documentElement;r.dataset.theme=t;r.style.colorScheme=t}catch(e){}})()";
21
+ export { DARK_QUERY };
package/lib/theme.js ADDED
@@ -0,0 +1,63 @@
1
+ // Light, dark or system, the same in every Bridge app. Decided 2026-11-03 (Aman): every web app offers dark
2
+ // mode; the choice is Light, Dark or System (the default, following the device); one choice applies across
3
+ // all Bridge apps, so it's kept in a cookie for the whole brdg.app domain. Built on Bridge OS's version
4
+ // (src/lib/hooks/use-theme.tsx and the init script in its root layout), which kept it per app in
5
+ // localStorage under "bridge-theme" and offered Light and Dark only; that key is still read, so people who
6
+ // chose dark in Bridge OS keep it.
7
+ export const THEME_COOKIE = "bridge-theme";
8
+ const ONE_YEAR = 60 * 60 * 24 * 365;
9
+ const DARK_QUERY = "(prefers-color-scheme: dark)";
10
+ function isPreference(v) {
11
+ return v === "light" || v === "dark" || v === "system";
12
+ }
13
+ /** The saved choice: the shared cookie, then Bridge OS's old per-app key, then System. */
14
+ export function readPreference() {
15
+ if (typeof document === "undefined")
16
+ return "system";
17
+ const match = document.cookie.match(new RegExp(`(?:^|; )${THEME_COOKIE}=(light|dark|system)`));
18
+ if (match && isPreference(match[1]))
19
+ return match[1];
20
+ try {
21
+ const old = localStorage.getItem(THEME_COOKIE);
22
+ if (isPreference(old))
23
+ return old;
24
+ }
25
+ catch {
26
+ // Storage blocked: fall through to System.
27
+ }
28
+ return "system";
29
+ }
30
+ /** Saves the choice for every Bridge app: on brdg.app it's shared by all subdomains; elsewhere (localhost, previews) by this site. */
31
+ export function writePreference(preference) {
32
+ const shared = /(^|\.)brdg\.app$/.test(location.hostname) ? "; Domain=.brdg.app" : "";
33
+ const secure = location.protocol === "https:" ? "; Secure" : "";
34
+ document.cookie = `${THEME_COOKIE}=${preference}; Path=/; Max-Age=${ONE_YEAR}; SameSite=Lax${shared}${secure}`;
35
+ }
36
+ export function systemTheme() {
37
+ return typeof window !== "undefined" && window.matchMedia(DARK_QUERY).matches ? "dark" : "light";
38
+ }
39
+ export function resolveTheme(preference) {
40
+ return preference === "system" ? systemTheme() : preference;
41
+ }
42
+ /**
43
+ * Puts the theme on <html>. Transitions are switched off for one frame while it changes, so every
44
+ * `transition-colors` on the page doesn't fade at once.
45
+ */
46
+ export function applyTheme(theme) {
47
+ const root = document.documentElement;
48
+ if (root.dataset.theme === theme)
49
+ return;
50
+ const pause = document.createElement("style");
51
+ pause.textContent = "*,*::before,*::after{transition:none!important;animation:none!important}";
52
+ document.head.appendChild(pause);
53
+ root.dataset.theme = theme;
54
+ root.style.colorScheme = theme;
55
+ void window.getComputedStyle(document.body).opacity;
56
+ requestAnimationFrame(() => pause.remove());
57
+ }
58
+ /**
59
+ * Runs in <head> before the page paints, so someone in dark never sees a white flash. The same steps as
60
+ * readPreference and resolveTheme, written out because it can't import anything.
61
+ */
62
+ export const themeScript = `(function(){try{var m=document.cookie.match(/(?:^|; )${THEME_COOKIE}=(light|dark|system)/),p=m?m[1]:null;if(!p){try{p=localStorage.getItem("${THEME_COOKIE}")}catch(e){}}if(p!=="light"&&p!=="dark")p="system";var t=p==="dark"||(p==="system"&&matchMedia("${DARK_QUERY}").matches)?"dark":"light",r=document.documentElement;r.dataset.theme=t;r.style.colorScheme=t}catch(e){}})()`;
63
+ export { DARK_QUERY };
package/lib/utils.d.ts ADDED
@@ -0,0 +1,22 @@
1
+ import type * as React from "react";
2
+ import { type ClassValue } from "clsx";
3
+ /**
4
+ * Bridge's class merger. Joins class names and lets a later class override an earlier one that sets
5
+ * the same thing, so `cn(base, className)` lets the caller win.
6
+ *
7
+ * Always import this one, never `clsx` or a local copy: when Bridge adds custom utilities (for example
8
+ * named text sizes), they get registered here with `extendTailwindMerge` so overrides keep working.
9
+ */
10
+ /** Bridge's named type sizes (globals.css → @theme). Registered so `text-13` counts as a size, not a color. */
11
+ declare const TYPE_SIZES: readonly ["11", "12", "13", "14", "16", "18", "22", "28", "36", "48"];
12
+ declare function cn(...inputs: ClassValue[]): string;
13
+ /**
14
+ * Extra attributes a component passes straight to its main element (the button you click, the field you
15
+ * type in): `id`, `aria-*`, `data-*` and event handlers the component doesn't own itself. The component's
16
+ * own props win where they overlap.
17
+ */
18
+ type ElementProps<E extends HTMLElement = HTMLElement> = Omit<React.HTMLAttributes<E>, "children" | "className" | "color" | "title" | "onClick" | "onChange" | "placeholder" | "defaultValue"> & {
19
+ [key: `data-${string}`]: string | undefined;
20
+ };
21
+ export { cn, TYPE_SIZES };
22
+ export type { ElementProps };
package/lib/utils.js ADDED
@@ -0,0 +1,27 @@
1
+ import { clsx } from "clsx";
2
+ import { extendTailwindMerge } from "tailwind-merge";
3
+ /**
4
+ * Bridge's class merger. Joins class names and lets a later class override an earlier one that sets
5
+ * the same thing, so `cn(base, className)` lets the caller win.
6
+ *
7
+ * Always import this one, never `clsx` or a local copy: when Bridge adds custom utilities (for example
8
+ * named text sizes), they get registered here with `extendTailwindMerge` so overrides keep working.
9
+ */
10
+ /** Bridge's named type sizes (globals.css → @theme). Registered so `text-13` counts as a size, not a color. */
11
+ const TYPE_SIZES = ["11", "12", "13", "14", "16", "18", "22", "28", "36", "48"];
12
+ // Bridge's page textures (globals.css): a pattern image and two overlays, not background colors. Registered so
13
+ // `cn("bg-[var(--info-bg)] bg-dot-grid")` keeps both instead of dropping the color, and the three can stack.
14
+ const twMerge = extendTailwindMerge({
15
+ extend: {
16
+ classGroups: {
17
+ "font-size": [{ text: [...TYPE_SIZES] }],
18
+ "bg-image": [{ bg: ["dot-grid"] }],
19
+ "bridge-noise": ["bg-noise"],
20
+ "bridge-glow": ["bg-glow-patches"],
21
+ },
22
+ },
23
+ });
24
+ function cn(...inputs) {
25
+ return twMerge(clsx(inputs));
26
+ }
27
+ export { cn, TYPE_SIZES };
package/package.json CHANGED
@@ -1,28 +1,44 @@
1
1
  {
2
2
  "name": "@brdgapp/ui",
3
- "version": "0.1.2",
4
- "description": "Shared Bridge UI components for Bridge apps.",
3
+ "version": "1.0.0",
4
+ "description": "Bridge's shared components, tokens and styles. Documented at handbook.brdg.app.",
5
5
  "license": "UNLICENSED",
6
+ "homepage": "https://handbook.brdg.app",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://gitlab.com/intropath/apps/bridge-design-system.git",
10
+ "directory": "packages/ui"
11
+ },
6
12
  "type": "module",
7
13
  "sideEffects": [
8
14
  "**/*.css"
9
15
  ],
10
- "files": [
11
- "src"
12
- ],
13
16
  "exports": {
14
- ".": "./src/index.ts",
15
- "./styles.css": "./src/styles.css"
16
- },
17
- "repository": {
18
- "type": "git",
19
- "url": "git+ssh://git@gitlab.com/intropath/packages/ui.git"
17
+ "./components/*": {
18
+ "types": "./components/*.d.ts",
19
+ "default": "./components/*.js"
20
+ },
21
+ "./hooks/*": {
22
+ "types": "./hooks/*.d.ts",
23
+ "default": "./hooks/*.js"
24
+ },
25
+ "./lib/*": {
26
+ "types": "./lib/*.d.ts",
27
+ "default": "./lib/*.js"
28
+ },
29
+ "./globals.css": "./styles/globals.css",
30
+ "./package.json": "./package.json"
20
31
  },
21
- "publishConfig": {
22
- "access": "public"
32
+ "dependencies": {
33
+ "@base-ui/react": "^1.9.0",
34
+ "class-variance-authority": "^0.7.1",
35
+ "clsx": "^2.1.1",
36
+ "lucide-react": "^1.6.0",
37
+ "tailwind-merge": "^3.7.0"
23
38
  },
24
39
  "peerDependencies": {
25
- "react": ">=18",
26
- "react-dom": ">=18"
40
+ "react": ">=19",
41
+ "react-dom": ">=19",
42
+ "tailwindcss": ">=4"
27
43
  }
28
- }
44
+ }