@bubbasfw/ui 0.1.0 → 0.2.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/dist/index.cjs CHANGED
@@ -4,11 +4,36 @@
4
4
  var react = require('react');
5
5
  var reactNative = require('react-native');
6
6
  var tailwindMerge = require('tailwind-merge');
7
+ var nativewind = require('nativewind');
7
8
  var jsxRuntime = require('nativewind/jsx-runtime');
8
9
 
10
+ function cn(...values) {
11
+ return tailwindMerge.twMerge(values.filter(Boolean).join(" "));
12
+ }
13
+ var focusRing = "web:focus-visible:outline web:focus-visible:outline-2 web:focus-visible:outline-offset-2 web:focus-visible:outline-ui-focus";
14
+ function useControllableState({ value, defaultValue, onChange }) {
15
+ const [local, setLocal] = react.useState(defaultValue);
16
+ const controlled = value !== void 0;
17
+ const current = controlled ? value : local;
18
+ const currentRef = react.useRef(current);
19
+ currentRef.current = current;
20
+ const onChangeRef = react.useRef(onChange);
21
+ onChangeRef.current = onChange;
22
+ const setValue = react.useCallback((next) => {
23
+ if (Object.is(currentRef.current, next)) return;
24
+ if (!controlled) {
25
+ currentRef.current = next;
26
+ setLocal(next);
27
+ }
28
+ onChangeRef.current?.(next);
29
+ }, [controlled]);
30
+ return [current, setValue];
31
+ }
32
+
9
33
  // src/tokens.ts
10
34
  var tokens = {
11
35
  brandVersion: "1.0.0",
36
+ systemVersion: "0.3.0",
12
37
  colors: {
13
38
  blue: "#007CE8",
14
39
  red: "#CC0000",
@@ -25,19 +50,133 @@ var tokens = {
25
50
  },
26
51
  spacing: { 1: 4, 2: 8, 3: 12, 4: 16, 6: 24, 8: 32, 12: 48, 16: 64 },
27
52
  radius: { control: 4, panel: 12 },
28
- touchTarget: { minimum: 44, default: 48 }
53
+ touchTarget: { minimum: 44, default: 48 },
54
+ layout: { maxWidth: 1200, readingWidth: 720, breakpoints: { compact: 640, wide: 960 } },
55
+ motion: { feedback: 140 },
56
+ themes: {
57
+ "light": {
58
+ "canvas": "#F7F8FA",
59
+ "surface": "#FFFFFF",
60
+ "raised": "#FFFFFF",
61
+ "hover": "#F6F7F9",
62
+ "selected": "#ECEEF1",
63
+ "text": "#131B23",
64
+ "heading": "#03256C",
65
+ "muted": "#526075",
66
+ "line": "#E4E6EA",
67
+ "border": "#637086",
68
+ "link": "#03256C",
69
+ "link-hover": "#A80000",
70
+ "focus": "#03256C",
71
+ "focus-inner": "#FFFFFF",
72
+ "accent": "#CC0000",
73
+ "action-text": "#FFFFFF",
74
+ "action-border": "#A80000",
75
+ "disabled-surface": "#F0F2F5",
76
+ "disabled-text": "#6B727E",
77
+ "info": "#03256C",
78
+ "info-surface": "#F1F5FC",
79
+ "info-border": "#03256C",
80
+ "success": "#23633F",
81
+ "success-surface": "#EAF5EF",
82
+ "success-border": "#23633F",
83
+ "warning": "#805500",
84
+ "warning-surface": "#FFF4D6",
85
+ "warning-border": "#805500",
86
+ "error": "#B42318",
87
+ "error-surface": "#FFF0EF",
88
+ "error-border": "#B42318",
89
+ "code-surface": "#171717",
90
+ "code-text": "#FFFFFF",
91
+ "code-muted": "#B4B4B4",
92
+ "selection": "#D9E8FB",
93
+ "selection-text": "#131B23",
94
+ "scrollbar": "#637086",
95
+ "scrollbar-track": "#F7F8FA",
96
+ "shadow-action": "0 3px 8px #CC000024",
97
+ "shadow-panel": "0 2px 12px #131B2308",
98
+ "shadow-dialog": "0 20px 100px #00133345"
99
+ },
100
+ "dark": {
101
+ "canvas": "#171717",
102
+ "surface": "#202020",
103
+ "raised": "#292929",
104
+ "hover": "#303030",
105
+ "selected": "#383838",
106
+ "text": "#FFFFFF",
107
+ "heading": "#FFFFFF",
108
+ "muted": "#B4B4B4",
109
+ "line": "#3D3D3D",
110
+ "border": "#858585",
111
+ "link": "#8DC7FF",
112
+ "link-hover": "#B8DEFF",
113
+ "focus": "#FFFFFF",
114
+ "focus-inner": "#202020",
115
+ "accent": "#FF9F99",
116
+ "action-text": "#FFFFFF",
117
+ "action-border": "#EF817A",
118
+ "disabled-surface": "#242424",
119
+ "disabled-text": "#9A9A9A",
120
+ "info": "#8DC7FF",
121
+ "info-surface": "#1D2D3D",
122
+ "info-border": "#8DC7FF",
123
+ "success": "#8DDBA9",
124
+ "success-surface": "#183323",
125
+ "success-border": "#8DDBA9",
126
+ "warning": "#FED766",
127
+ "warning-surface": "#3A2E12",
128
+ "warning-border": "#FED766",
129
+ "error": "#FFAEA8",
130
+ "error-surface": "#3D2020",
131
+ "error-border": "#FFAEA8",
132
+ "code-surface": "#171717",
133
+ "code-text": "#FFFFFF",
134
+ "code-muted": "#B4B4B4",
135
+ "selection": "#383838",
136
+ "selection-text": "#FFFFFF",
137
+ "scrollbar": "#858585",
138
+ "scrollbar-track": "#171717",
139
+ "shadow-action": "0 3px 8px #00000040",
140
+ "shadow-panel": "0 2px 12px #00000020",
141
+ "shadow-dialog": "0 20px 100px #00000080"
142
+ }
143
+ }
29
144
  };
145
+ function rgb(hex) {
146
+ return [1, 3, 5].map((start) => parseInt(hex.slice(start, start + 2), 16)).join(" ");
147
+ }
148
+ function themeVariables(appearance) {
149
+ return nativewind.vars(Object.fromEntries(Object.entries(tokens.themes[appearance]).filter(([, value]) => value.startsWith("#")).map(([key, value]) => [`--bui-${key}`, rgb(value)])));
150
+ }
151
+ function themeValue(appearance, resolvedAppearance) {
152
+ return {
153
+ appearance,
154
+ resolvedAppearance,
155
+ colors: Object.fromEntries(Object.entries(tokens.themes[resolvedAppearance]).filter(([, value]) => value.startsWith("#"))),
156
+ variables: themeVariables(resolvedAppearance)
157
+ };
158
+ }
159
+ var ThemeContext = react.createContext(themeValue("light", "light"));
160
+ function ThemeProvider({ appearance = "light", children, className, style, ...props }) {
161
+ const system = reactNative.useColorScheme();
162
+ const resolved = appearance === "system" ? system === "dark" ? "dark" : "light" : appearance;
163
+ const value = react.useMemo(() => themeValue(appearance, resolved), [appearance, resolved]);
164
+ return /* @__PURE__ */ jsxRuntime.jsx(ThemeContext.Provider, { value, children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { ...props, className: cn("bg-ui-canvas", className), style: [value.variables, style], children }) });
165
+ }
166
+ function useTheme() {
167
+ return react.useContext(ThemeContext);
168
+ }
30
169
  var containerVariants = {
31
- primary: "border-bubba-red bg-bubba-red",
32
- secondary: "border-bubba-navy bg-bubba-navy",
33
- outline: "border-bubba-navy bg-transparent",
170
+ primary: "border-ui-action-border bg-bubba-red",
171
+ secondary: "border-ui-border bg-ui-selected",
172
+ outline: "border-ui-border bg-transparent",
34
173
  ghost: "border-transparent bg-transparent"
35
174
  };
36
175
  var textVariants = {
37
- primary: "text-bubba-white",
38
- secondary: "text-bubba-white",
39
- outline: "text-bubba-navy",
40
- ghost: "text-bubba-navy"
176
+ primary: "text-ui-action-text",
177
+ secondary: "text-ui-text",
178
+ outline: "text-ui-text",
179
+ ghost: "text-ui-link"
41
180
  };
42
181
  var containerSizes = {
43
182
  sm: "min-h-[44px] px-3 py-2",
@@ -64,10 +203,11 @@ var Button = react.forwardRef(
64
203
  "aria-disabled": ariaDisabled,
65
204
  ...props
66
205
  }, ref) {
206
+ const { colors } = useTheme();
67
207
  const explicitlyDisabled = disabled || (ariaDisabled ?? accessibilityState?.disabled ?? false);
68
208
  const isDisabled = explicitlyDisabled || loading;
69
209
  const isBusy = loading || (ariaBusy ?? accessibilityState?.busy ?? false);
70
- const spinnerColor = explicitlyDisabled ? tokens.colors.disabledText : variant === "primary" || variant === "secondary" ? tokens.colors.white : tokens.colors.navy;
210
+ const spinnerColor = explicitlyDisabled ? colors["disabled-text"] : variant === "primary" ? colors["action-text"] : colors.text;
71
211
  return /* @__PURE__ */ jsxRuntime.jsxs(
72
212
  reactNative.Pressable,
73
213
  {
@@ -78,11 +218,12 @@ var Button = react.forwardRef(
78
218
  "aria-busy": isBusy,
79
219
  "aria-disabled": isDisabled,
80
220
  disabled: isDisabled,
81
- className: tailwindMerge.twMerge(
82
- "min-w-[44px] flex-row items-center justify-center gap-2 rounded-[4px] border active:opacity-80 web:focus-visible:outline web:focus-visible:outline-2 web:focus-visible:outline-offset-2 web:focus-visible:outline-bubba-navy",
221
+ className: cn(
222
+ "min-w-[44px] flex-row items-center justify-center gap-2 rounded-[4px] border active:opacity-80",
223
+ focusRing,
83
224
  containerVariants[variant],
84
225
  containerSizes[size],
85
- explicitlyDisabled && "border-bubba-disabled bg-bubba-disabled",
226
+ explicitlyDisabled && "border-ui-disabled-surface bg-ui-disabled-surface",
86
227
  className
87
228
  ),
88
229
  children: [
@@ -90,11 +231,11 @@ var Button = react.forwardRef(
90
231
  /* @__PURE__ */ jsxRuntime.jsx(
91
232
  reactNative.Text,
92
233
  {
93
- className: tailwindMerge.twMerge(
234
+ className: cn(
94
235
  "font-bold text-center",
95
236
  textVariants[variant],
96
237
  textSizes[size],
97
- explicitlyDisabled && "text-bubba-disabledText",
238
+ explicitlyDisabled && "text-ui-disabled-text",
98
239
  textClassName
99
240
  ),
100
241
  children: loading && loadingLabel ? loadingLabel : children
@@ -105,6 +246,957 @@ var Button = react.forwardRef(
105
246
  );
106
247
  }
107
248
  );
249
+ var tones = { default: "text-ui-text", muted: "text-ui-muted", info: "text-ui-info", success: "text-ui-success", warning: "text-ui-warning", error: "text-ui-error" };
250
+ var variants = { body: "text-base leading-6", lead: "text-lg leading-7", small: "text-sm leading-5", label: "text-base leading-6 font-semibold", caption: "text-xs leading-4", code: "font-mono text-sm leading-6" };
251
+ var Text2 = react.forwardRef(function Text3({ tone = "default", variant = "body", className, ...props }, ref) {
252
+ return /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { ...props, ref, className: cn(variants[variant], tones[tone], className) });
253
+ });
254
+ var levels = { 1: "text-3xl leading-10", 2: "text-2xl leading-8", 3: "text-xl leading-7", 4: "text-lg leading-6", 5: "text-base leading-6", 6: "text-sm leading-5" };
255
+ var Heading = react.forwardRef(function Heading2({ level = 2, className, ...props }, ref) {
256
+ const webLevel = reactNative.Platform.OS === "web" ? { "aria-level": level } : {};
257
+ return /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { ...props, ...webLevel, ref, accessibilityRole: "header", className: cn("font-bold text-ui-heading", levels[level], className) });
258
+ });
259
+ var gaps = { 0: "gap-0", 1: "gap-1", 2: "gap-2", 3: "gap-3", 4: "gap-4", 6: "gap-6", 8: "gap-8", 12: "gap-12", 16: "gap-16" };
260
+ var alignments = { start: "items-start", center: "items-center", end: "items-end", stretch: "items-stretch" };
261
+ var justifications = { start: "justify-start", center: "justify-center", end: "justify-end", between: "justify-between" };
262
+ var Stack = react.forwardRef(function Stack2({ gap = 4, align = "stretch", justify = "start", className, ...props }, ref) {
263
+ return /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { ...props, ref, className: cn("flex-col", gaps[gap], alignments[align], justifications[justify], className) });
264
+ });
265
+ var Inline = react.forwardRef(function Inline2({ gap = 3, align = "center", justify = "start", wrap = true, className, ...props }, ref) {
266
+ return /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { ...props, ref, className: cn("flex-row", gaps[gap], alignments[align], justifications[justify], wrap ? "flex-wrap" : "flex-nowrap", className) });
267
+ });
268
+ var Container = react.forwardRef(function Container2({ reading = false, className, ...props }, ref) {
269
+ return /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { ...props, ref, className: cn("mx-auto w-full px-4 sm:px-6 lg:px-8", reading ? "max-w-[720px]" : "max-w-[1200px]", className) });
270
+ });
271
+ var paddings = { 0: "p-0", 1: "p-1", 2: "p-2", 3: "p-3", 4: "p-4", 6: "p-6", 8: "p-8", 12: "p-12", 16: "p-16" };
272
+ var Card = react.forwardRef(function Card2({ padding = 6, className, ...props }, ref) {
273
+ return /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { ...props, ref, className: cn("rounded-xl border border-ui-line bg-ui-surface", paddings[padding], className) });
274
+ });
275
+ function Divider({ orientation = "horizontal", className, ...props }) {
276
+ return /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { ...props, accessible: false, className: cn("bg-ui-line", orientation === "horizontal" ? "h-px w-full" : "w-px self-stretch", className) });
277
+ }
278
+ var iconVariants = { primary: "bg-bubba-red border-bubba-red", outline: "border-ui-border bg-transparent", ghost: "border-transparent bg-transparent" };
279
+ var IconButton = react.forwardRef(function IconButton2({ accessibilityLabel, children, variant = "ghost", className, disabled, accessibilityState, "aria-disabled": ariaDisabled, ...props }, ref) {
280
+ const isDisabled = disabled || (ariaDisabled ?? accessibilityState?.disabled ?? false);
281
+ return /* @__PURE__ */ jsxRuntime.jsx(reactNative.Pressable, { ...props, ref, accessibilityRole: "button", accessibilityLabel, disabled: isDisabled, "aria-disabled": isDisabled, accessibilityState: { ...accessibilityState, disabled: isDisabled }, className: cn("h-12 w-12 items-center justify-center rounded border active:opacity-80", focusRing, iconVariants[variant], isDisabled && "opacity-50", className), children });
282
+ });
283
+ var LinkPressable = reactNative.Pressable;
284
+ var Link = react.forwardRef(function Link2({ href, children, external = false, className, textClassName, disabled, onPress, onOpenError, accessibilityState, "aria-disabled": ariaDisabled, ...props }, ref) {
285
+ const isDisabled = disabled || (ariaDisabled ?? accessibilityState?.disabled ?? false);
286
+ const webProps = reactNative.Platform.OS === "web" && !isDisabled ? { href, hrefAttrs: external ? { target: "_blank", rel: "noopener noreferrer" } : void 0 } : {};
287
+ return /* @__PURE__ */ jsxRuntime.jsx(LinkPressable, { ...props, ...webProps, ref, accessibilityRole: "link", disabled: isDisabled, "aria-disabled": isDisabled, accessibilityState: { ...accessibilityState, disabled: isDisabled }, className: cn("min-h-[44px] min-w-[44px] justify-center self-start", focusRing, isDisabled && "opacity-50", className), onPress: (event) => {
288
+ onPress?.(event);
289
+ if (reactNative.Platform.OS !== "web" && !event.isDefaultPrevented()) void reactNative.Linking.openURL(href).catch((error) => onOpenError?.(error));
290
+ }, children: /* @__PURE__ */ jsxRuntime.jsx(Text2, { className: cn("text-ui-link underline", textClassName), children }) });
291
+ });
292
+ var toneClasses = {
293
+ neutral: "bg-ui-selected",
294
+ info: "bg-ui-info-surface",
295
+ success: "bg-ui-success-surface",
296
+ warning: "bg-ui-warning-surface",
297
+ error: "bg-ui-error-surface"
298
+ };
299
+ var toneText = { neutral: "text-ui-text", info: "text-ui-info", success: "text-ui-success", warning: "text-ui-warning", error: "text-ui-error" };
300
+ function Badge({ children, tone = "neutral", className, textClassName, ...props }) {
301
+ return /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { ...props, className: cn("self-start rounded-full px-3 py-1", toneClasses[tone], className), children: /* @__PURE__ */ jsxRuntime.jsx(Text2, { variant: "small", className: cn("font-semibold", toneText[tone], textClassName), children }) });
302
+ }
303
+ var avatarSizes = { sm: "h-8 w-8", md: "h-12 w-12", lg: "h-16 w-16" };
304
+ function Avatar({ source, name, size = "md", className, ...props }) {
305
+ const [failedSource, setFailedSource] = react.useState();
306
+ const initials = name.trim().split(/\s+/).slice(0, 2).map((part) => part[0]).join("").toUpperCase() || "?";
307
+ return /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { ...props, accessible: true, accessibilityRole: "image", accessibilityLabel: name, className: cn("items-center justify-center overflow-hidden rounded-full bg-ui-selected", avatarSizes[size], className), children: source && source !== failedSource ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Image, { source, resizeMode: "cover", accessible: false, className: "h-full w-full", onError: () => setFailedSource(source) }) : /* @__PURE__ */ jsxRuntime.jsx(Text2, { accessible: false, className: "font-bold", children: initials }) });
308
+ }
309
+ function EmptyState({ title, description, children, className, ...props }) {
310
+ return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { ...props, className: cn("items-start gap-3 py-8", className), children: [
311
+ /* @__PURE__ */ jsxRuntime.jsx(Text2, { className: "text-lg font-bold", children: title }),
312
+ description ? /* @__PURE__ */ jsxRuntime.jsx(Text2, { tone: "muted", children: description }) : null,
313
+ children
314
+ ] });
315
+ }
316
+ var ListItem = react.forwardRef(function ListItem2({ title, description, leading, trailing, className, disabled = false, onPress, accessibilityState, ...props }, ref) {
317
+ const content = /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
318
+ leading,
319
+ /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { className: "min-w-0 flex-1 gap-1", children: [
320
+ /* @__PURE__ */ jsxRuntime.jsx(Text2, { className: "font-semibold", children: title }),
321
+ description ? /* @__PURE__ */ jsxRuntime.jsx(Text2, { variant: "small", tone: "muted", children: description }) : null
322
+ ] }),
323
+ trailing
324
+ ] });
325
+ const classes = cn("min-h-[48px] min-w-[44px] flex-row items-center gap-3 border-b border-ui-line py-3", onPress && focusRing, disabled && "opacity-50", className);
326
+ return onPress ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Pressable, { ...props, ref, onPress, disabled, "aria-disabled": disabled, accessibilityRole: "button", accessibilityState: { ...accessibilityState, disabled }, className: classes, children: content }) : /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { ...props, ref, accessibilityState, className: classes, children: content });
327
+ });
328
+ function Accordion({ items, value, defaultValue = "", onValueChange, className, ...props }) {
329
+ const [selected, setSelected] = useControllableState({ value, defaultValue, onChange: onValueChange });
330
+ const id = react.useId();
331
+ return /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { ...props, className, children: items.map((item, index) => {
332
+ const expanded = selected === item.value;
333
+ const triggerID = `${id}-trigger-${index}`;
334
+ const panelID = `${id}-panel-${index}`;
335
+ return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { className: "border-b border-ui-line", children: [
336
+ /* @__PURE__ */ jsxRuntime.jsxs(reactNative.Pressable, { nativeID: triggerID, accessibilityRole: "button", accessibilityLabel: item.title, accessibilityState: { expanded, disabled: item.disabled }, "aria-expanded": expanded, "aria-disabled": item.disabled, ...{ "aria-controls": expanded ? panelID : void 0 }, disabled: item.disabled, onPress: () => setSelected(expanded ? "" : item.value), className: cn("min-h-[48px] min-w-[44px] flex-row items-center justify-between gap-4 py-3", focusRing, item.disabled && "opacity-50"), children: [
337
+ /* @__PURE__ */ jsxRuntime.jsx(Text2, { className: "flex-1 font-semibold", children: item.title }),
338
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { accessible: false, className: cn("mr-1 h-2 w-2 border-b-2 border-r-2 border-ui-text", expanded ? "rotate-[225deg]" : "rotate-45") })
339
+ ] }),
340
+ expanded ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { nativeID: panelID, "aria-labelledby": triggerID, className: "gap-3 pb-4", children: item.content }) : null
341
+ ] }, item.value);
342
+ }) });
343
+ }
344
+ var locks = /* @__PURE__ */ new WeakMap();
345
+ var overflowProperties = ["overflow", "overflow-x", "overflow-y"];
346
+ var overscrollProperties = ["overscroll-behavior", "overscroll-behavior-x", "overscroll-behavior-y"];
347
+ function preserve(style, properties) {
348
+ const previous = properties.map((property) => ({
349
+ property,
350
+ value: style.getPropertyValue(property),
351
+ priority: style.getPropertyPriority(property)
352
+ }));
353
+ return () => {
354
+ for (const { property, value, priority } of previous) {
355
+ if (value) style.setProperty(property, value, priority);
356
+ else style.removeProperty(property);
357
+ }
358
+ };
359
+ }
360
+ function acquire(document) {
361
+ let lock = locks.get(document);
362
+ if (!lock) {
363
+ const body = document.body;
364
+ const view = document.defaultView;
365
+ if (!body || !view) return;
366
+ const root = document.documentElement;
367
+ const { scrollX, scrollY } = view;
368
+ const width = root.clientWidth;
369
+ const paddingRight = Number.parseFloat(view.getComputedStyle(body).paddingRight) || 0;
370
+ const restoreRoot = preserve(root.style, [...overflowProperties, ...overscrollProperties]);
371
+ const restoreBody = preserve(body.style, [
372
+ ...overflowProperties,
373
+ ...overscrollProperties,
374
+ "position",
375
+ "top",
376
+ "left",
377
+ "right",
378
+ "padding-right"
379
+ ]);
380
+ const restoreScrollBehavior = preserve(root.style, ["scroll-behavior"]);
381
+ root.style.setProperty("scroll-behavior", "auto", "important");
382
+ root.style.setProperty("overflow", "hidden", "important");
383
+ root.style.setProperty("overscroll-behavior", "none", "important");
384
+ body.style.setProperty("overflow", "hidden", "important");
385
+ body.style.setProperty("overscroll-behavior", "none", "important");
386
+ body.style.setProperty("position", "fixed", "important");
387
+ body.style.setProperty("top", `${-scrollY}px`, "important");
388
+ body.style.setProperty("left", `${-scrollX}px`, "important");
389
+ body.style.setProperty("right", `${scrollX}px`, "important");
390
+ const compensation = Math.max(0, root.clientWidth - width);
391
+ if (compensation > 0) body.style.setProperty("padding-right", `${paddingRight + compensation}px`, "important");
392
+ lock = {
393
+ count: 0,
394
+ restore: () => {
395
+ restoreRoot();
396
+ restoreBody();
397
+ view.scrollTo(scrollX, scrollY);
398
+ restoreScrollBehavior();
399
+ }
400
+ };
401
+ locks.set(document, lock);
402
+ }
403
+ lock.count += 1;
404
+ let released = false;
405
+ return () => {
406
+ if (released) return;
407
+ released = true;
408
+ lock.count -= 1;
409
+ if (lock.count === 0) {
410
+ lock.restore();
411
+ locks.delete(document);
412
+ }
413
+ };
414
+ }
415
+ function useWebScrollLock(active) {
416
+ react.useEffect(() => {
417
+ if (!active || reactNative.Platform.OS !== "web") return;
418
+ const document = globalThis.document;
419
+ return document ? acquire(document) : void 0;
420
+ }, [active]);
421
+ }
422
+ function Overlay({ open, defaultOpen = false, onOpenChange, title, description, children, closeLabel = "Close", dismissOnBackdropPress = true, className, contentClassName, testID, style, sheet = false, ...props }) {
423
+ const [isOpen, setOpen] = useControllableState({ value: open, defaultValue: defaultOpen, onChange: onOpenChange });
424
+ const { variables } = useTheme();
425
+ const titleID = react.useId();
426
+ const descriptionID = react.useId();
427
+ const [webMounted, setWebMounted] = react.useState(false);
428
+ react.useEffect(() => {
429
+ if (reactNative.Platform.OS === "web") setWebMounted(true);
430
+ }, []);
431
+ const [, refreshPortal] = react.useReducer((count) => count + 1, 0);
432
+ react.useEffect(() => {
433
+ if (reactNative.Platform.OS === "web" && isOpen && webMounted) refreshPortal();
434
+ }, [isOpen, webMounted]);
435
+ const canRender = reactNative.Platform.OS !== "web" || webMounted;
436
+ useWebScrollLock(isOpen && canRender);
437
+ if (!isOpen || !canRender) return null;
438
+ return /* @__PURE__ */ jsxRuntime.jsx(reactNative.Modal, { transparent: true, visible: true, animationType: "none", onRequestClose: () => setOpen(false), accessibilityLabel: title, "aria-labelledby": titleID, ...description ? { "aria-describedby": descriptionID } : {}, children: /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { className: cn("flex-1 px-4 py-6", sheet ? "justify-end" : "justify-center"), style: variables, children: [
439
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { testID: testID ? `${testID}-backdrop` : void 0, accessible: false, onStartShouldSetResponder: () => dismissOnBackdropPress, onResponderRelease: () => {
440
+ if (dismissOnBackdropPress) setOpen(false);
441
+ }, className: "absolute inset-0 bg-black/50" }),
442
+ /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { ...props, testID, accessibilityViewIsModal: true, className: cn("w-full self-center bg-ui-surface p-6", sheet ? "max-h-[85%] max-w-[720px] rounded-t-xl" : "max-h-[85%] max-w-[560px] rounded-xl", className), style, children: [
443
+ /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { className: "mb-4 flex-row items-start justify-between gap-4", children: [
444
+ /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { className: "min-w-0 flex-1 gap-2", children: [
445
+ /* @__PURE__ */ jsxRuntime.jsx(Heading, { nativeID: titleID, level: 2, children: title }),
446
+ description ? /* @__PURE__ */ jsxRuntime.jsx(Text2, { nativeID: descriptionID, tone: "muted", children: description }) : null
447
+ ] }),
448
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Pressable, { accessibilityRole: "button", accessibilityLabel: closeLabel, onPress: () => setOpen(false), className: cn("min-h-[44px] min-w-[44px] items-center justify-center rounded px-2", focusRing), children: /* @__PURE__ */ jsxRuntime.jsx(Text2, { variant: "small", className: "font-semibold text-ui-link", children: closeLabel }) })
449
+ ] }),
450
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.ScrollView, { keyboardShouldPersistTaps: "handled", className: cn("shrink", contentClassName), children })
451
+ ] })
452
+ ] }) });
453
+ }
454
+ function Dialog(props) {
455
+ return /* @__PURE__ */ jsxRuntime.jsx(Overlay, { ...props });
456
+ }
457
+ function Sheet(props) {
458
+ return /* @__PURE__ */ jsxRuntime.jsx(Overlay, { ...props, sheet: true });
459
+ }
460
+ var paints = { neutral: "bg-ui-selected", info: "bg-ui-info-surface", success: "bg-ui-success-surface", warning: "bg-ui-warning-surface", error: "bg-ui-error-surface" };
461
+ var texts = { neutral: "text-ui-text", info: "text-ui-info", success: "text-ui-success", warning: "text-ui-warning", error: "text-ui-error" };
462
+ function Alert({ title, description, children, tone = "info", announce = "off", className, ...props }) {
463
+ return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { ...props, accessibilityLiveRegion: announce === "off" ? "none" : announce, "aria-live": announce, className: cn("gap-2 rounded-xl p-4", paints[tone], className), children: [
464
+ /* @__PURE__ */ jsxRuntime.jsx(Text2, { className: cn("font-semibold", texts[tone]), children: title }),
465
+ description ? /* @__PURE__ */ jsxRuntime.jsx(Text2, { className: texts[tone], children: description }) : null,
466
+ children
467
+ ] });
468
+ }
469
+ function Toast({ open, defaultOpen = true, onOpenChange, duration = 0, dismissLabel = "Dismiss notification", className, children, ...props }) {
470
+ const [visible, setVisible] = useControllableState({ value: open, defaultValue: defaultOpen, onChange: onOpenChange });
471
+ react.useEffect(() => {
472
+ if (!visible || duration <= 0) return;
473
+ const timer = setTimeout(() => setVisible(false), duration);
474
+ return () => clearTimeout(timer);
475
+ }, [duration, visible, setVisible]);
476
+ if (!visible) return null;
477
+ return /* @__PURE__ */ jsxRuntime.jsxs(Alert, { ...props, accessibilityRole: "alert", announce: "polite", className, children: [
478
+ children,
479
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Pressable, { accessibilityRole: "button", accessibilityLabel: dismissLabel, onPress: () => setVisible(false), className: cn("min-h-[44px] min-w-[44px] self-start justify-center rounded px-2", focusRing), children: /* @__PURE__ */ jsxRuntime.jsx(Text2, { variant: "small", className: "font-semibold underline", children: "Dismiss" }) })
480
+ ] });
481
+ }
482
+ function Spinner({ label = "Loading", size = "small", className, ...props }) {
483
+ const { colors } = useTheme();
484
+ return /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { ...props, accessibilityRole: "progressbar", accessibilityLabel: label, accessibilityState: { busy: true }, "aria-busy": true, className: cn("self-start", className), children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.ActivityIndicator, { accessible: false, color: colors.text, size }) });
485
+ }
486
+ function Progress({ value, max = 100, label, className, indicatorClassName, ...props }) {
487
+ const maximum = Number.isFinite(max) && max > 0 ? max : 100;
488
+ const current = Math.min(maximum, Math.max(0, Number.isFinite(value) ? value : 0));
489
+ return /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { ...props, accessibilityRole: "progressbar", accessibilityLabel: label, accessibilityValue: { min: 0, max: maximum, now: current }, "aria-valuemin": 0, "aria-valuemax": maximum, "aria-valuenow": current, className: cn("h-2 w-full overflow-hidden rounded-full bg-ui-selected", className), children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { className: cn("h-full bg-bubba-red", indicatorClassName), style: { width: `${current / maximum * 100}%` } }) });
490
+ }
491
+ function Skeleton({ className, ...props }) {
492
+ return /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { ...props, accessible: false, "aria-hidden": true, importantForAccessibility: "no-hide-descendants", className: cn("h-4 w-full rounded bg-ui-selected", className) });
493
+ }
494
+ function Breadcrumbs({ items, label = "Breadcrumb", className, ...props }) {
495
+ return /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { ...props, role: "navigation", accessibilityLabel: label, className: cn("flex-row flex-wrap items-center gap-2", className), children: items.map((item, index) => /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { className: "flex-row items-center gap-2", children: [
496
+ index > 0 ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { accessible: false, className: "h-2 w-2 rotate-[-45deg] border-b border-r border-ui-muted" }) : null,
497
+ item.href && index !== items.length - 1 ? /* @__PURE__ */ jsxRuntime.jsx(Link, { href: item.href, textClassName: "text-sm", children: item.label }) : /* @__PURE__ */ jsxRuntime.jsx(Text2, { variant: "small", ...index === items.length - 1 ? { "aria-current": "page" } : {}, children: item.label })
498
+ ] }, `${item.label}-${index}`)) });
499
+ }
500
+ function Pagination({ page, count, onPageChange, label = "Pagination", className, ...props }) {
501
+ const total = Math.max(1, Math.floor(Number.isFinite(count) ? count : 1));
502
+ const current = Math.min(total, Math.max(1, Math.floor(Number.isFinite(page) ? page : 1)));
503
+ return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { ...props, role: "navigation", accessibilityLabel: label, className: cn("flex-row flex-wrap items-center gap-3", className), children: [
504
+ /* @__PURE__ */ jsxRuntime.jsx(Button, { variant: "outline", size: "sm", disabled: current === 1, onPress: () => onPageChange(current - 1), accessibilityLabel: "Previous page", children: "Previous" }),
505
+ /* @__PURE__ */ jsxRuntime.jsx(Text2, { variant: "small", accessibilityLiveRegion: "polite", "aria-live": "polite", children: `Page ${current} of ${total}` }),
506
+ /* @__PURE__ */ jsxRuntime.jsx(Button, { variant: "outline", size: "sm", disabled: current === total, onPress: () => onPageChange(current + 1), accessibilityLabel: "Next page", children: "Next" })
507
+ ] });
508
+ }
509
+ var FieldContext = react.createContext(void 0);
510
+ function useFormField() {
511
+ return react.useContext(FieldContext);
512
+ }
513
+ var FormField = react.forwardRef(function FormField2({
514
+ label,
515
+ hint,
516
+ error,
517
+ required = false,
518
+ disabled = false,
519
+ announceError = false,
520
+ children,
521
+ nativeID,
522
+ className,
523
+ labelClassName,
524
+ ...props
525
+ }, ref) {
526
+ const generatedID = react.useId().replace(/:/g, "");
527
+ const id = nativeID ?? `bui-field-${generatedID}`;
528
+ const descriptionID = [hint && `${id}-hint`, error && `${id}-error`].filter(Boolean).join(" ") || void 0;
529
+ const field = {
530
+ controlID: `${id}-control`,
531
+ labelID: `${id}-label`,
532
+ label,
533
+ descriptionID,
534
+ hint,
535
+ error,
536
+ invalid: Boolean(error),
537
+ required,
538
+ disabled
539
+ };
540
+ return /* @__PURE__ */ jsxRuntime.jsx(FieldContext.Provider, { value: field, children: /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { ...props, ref, nativeID, className: cn("gap-2", className), children: [
541
+ /* @__PURE__ */ jsxRuntime.jsxs(reactNative.Text, { nativeID: field.labelID, className: cn("text-base font-semibold text-ui-text", labelClassName), children: [
542
+ label,
543
+ required ? " *" : ""
544
+ ] }),
545
+ children,
546
+ hint ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { nativeID: `${id}-hint`, className: "text-sm leading-5 text-ui-muted", children: hint }) : null,
547
+ error ? /* @__PURE__ */ jsxRuntime.jsx(
548
+ reactNative.Text,
549
+ {
550
+ nativeID: `${id}-error`,
551
+ accessibilityLiveRegion: announceError ? "polite" : "none",
552
+ ...reactNative.Platform.OS === "web" ? { "aria-live": announceError ? "polite" : "off" } : {},
553
+ className: "text-sm leading-5 text-ui-error",
554
+ children: error
555
+ }
556
+ ) : null
557
+ ] }) });
558
+ });
559
+ var Input = react.forwardRef(function Input2({
560
+ disabled,
561
+ invalid,
562
+ required,
563
+ readOnly = false,
564
+ editable,
565
+ className,
566
+ nativeID,
567
+ accessibilityLabel,
568
+ accessibilityHint,
569
+ accessibilityState,
570
+ "aria-label": ariaLabel,
571
+ "aria-disabled": ariaDisabled,
572
+ placeholderTextColor,
573
+ selectionColor,
574
+ ...props
575
+ }, ref) {
576
+ const field = useFormField();
577
+ const { colors } = useTheme();
578
+ const isDisabled = Boolean(disabled || ariaDisabled || accessibilityState?.disabled || field?.disabled);
579
+ const isReadOnly = readOnly || editable === false && !isDisabled;
580
+ const isInvalid = invalid ?? field?.invalid ?? false;
581
+ const isRequired = required ?? field?.required ?? false;
582
+ const explicitLabel = accessibilityLabel ?? ariaLabel;
583
+ const nativeHint = [
584
+ accessibilityHint ?? field?.hint,
585
+ field?.error && `Error: ${field.error}`,
586
+ isRequired && "Required",
587
+ isReadOnly && "Read only"
588
+ ].filter(Boolean).join(". ") || void 0;
589
+ const webProps = reactNative.Platform.OS === "web" ? {
590
+ disabled: isDisabled,
591
+ readOnly: isReadOnly,
592
+ "aria-describedby": field?.descriptionID,
593
+ "aria-labelledby": explicitLabel ? void 0 : field?.labelID,
594
+ "aria-invalid": isInvalid,
595
+ "aria-required": isRequired,
596
+ "aria-readonly": isReadOnly || void 0
597
+ } : {};
598
+ return /* @__PURE__ */ jsxRuntime.jsx(
599
+ reactNative.TextInput,
600
+ {
601
+ ...props,
602
+ ...webProps,
603
+ ref,
604
+ nativeID: nativeID ?? field?.controlID,
605
+ accessibilityLabel: explicitLabel ?? (reactNative.Platform.OS === "web" ? void 0 : field?.label),
606
+ accessibilityHint: nativeHint,
607
+ accessibilityState: { ...accessibilityState, disabled: isDisabled },
608
+ "aria-disabled": isDisabled,
609
+ editable: !isDisabled && !isReadOnly,
610
+ placeholderTextColor: placeholderTextColor ?? colors.muted,
611
+ selectionColor: selectionColor ?? colors.selection,
612
+ className: cn(
613
+ "min-h-[48px] rounded-[4px] border border-ui-border bg-ui-surface px-3 py-3 text-base text-ui-text",
614
+ focusRing,
615
+ isInvalid && "border-ui-error",
616
+ isDisabled && "bg-ui-disabled-surface text-ui-disabled-text",
617
+ className
618
+ )
619
+ }
620
+ );
621
+ });
622
+ var Textarea = react.forwardRef(function Textarea2({
623
+ className,
624
+ numberOfLines = 4,
625
+ textAlignVertical = "top",
626
+ ...props
627
+ }, ref) {
628
+ return /* @__PURE__ */ jsxRuntime.jsx(
629
+ Input,
630
+ {
631
+ ...props,
632
+ ref,
633
+ multiline: true,
634
+ numberOfLines,
635
+ textAlignVertical,
636
+ className: cn("min-h-[120px]", className)
637
+ }
638
+ );
639
+ });
640
+ function spaceKeyHandler(disabled, activate) {
641
+ return reactNative.Platform.OS === "web" ? {
642
+ onKeyDown(event) {
643
+ const key = event.key;
644
+ if (key === " " || key === "Spacebar") {
645
+ event.preventDefault();
646
+ if (!disabled && !event.repeat) activate(event);
647
+ }
648
+ }
649
+ } : {};
650
+ }
651
+ var ChoiceControl = react.forwardRef(function ChoiceControl2({
652
+ kind,
653
+ checked,
654
+ defaultChecked = false,
655
+ onCheckedChange,
656
+ indeterminate = false,
657
+ label,
658
+ description,
659
+ invalid,
660
+ required,
661
+ disabled,
662
+ className,
663
+ textClassName,
664
+ onPress,
665
+ accessibilityLabel,
666
+ accessibilityHint,
667
+ accessibilityState,
668
+ "aria-label": ariaLabel,
669
+ "aria-disabled": ariaDisabled,
670
+ nativeID,
671
+ ...props
672
+ }, ref) {
673
+ const field = useFormField();
674
+ const generatedID = react.useId().replace(/:/g, "");
675
+ const id = nativeID ?? field?.controlID ?? `bui-choice-${generatedID}`;
676
+ const isDisabled = Boolean(disabled || ariaDisabled || accessibilityState?.disabled || field?.disabled);
677
+ const [isChecked, setChecked] = useControllableState({ value: checked, defaultValue: defaultChecked, onChange: onCheckedChange });
678
+ const mixed = kind === "checkbox" && indeterminate;
679
+ const state = mixed ? "mixed" : isChecked;
680
+ const name = accessibilityLabel ?? ariaLabel ?? label ?? field?.label;
681
+ const isRequired = required ?? field?.required ?? false;
682
+ const isInvalid = invalid ?? field?.invalid ?? false;
683
+ const hint = [
684
+ accessibilityHint ?? description ?? field?.hint,
685
+ field?.error && `Error: ${field.error}`,
686
+ isRequired && "Required"
687
+ ].filter(Boolean).join(". ") || void 0;
688
+ const activate = (event) => {
689
+ if (isDisabled) return;
690
+ setChecked(mixed || !isChecked);
691
+ onPress?.(event);
692
+ };
693
+ const descriptions = [description && `${id}-description`, field?.descriptionID].filter(Boolean).join(" ") || void 0;
694
+ return /* @__PURE__ */ jsxRuntime.jsxs(
695
+ reactNative.Pressable,
696
+ {
697
+ ...props,
698
+ ...spaceKeyHandler(isDisabled, activate),
699
+ ref,
700
+ nativeID: id,
701
+ accessibilityRole: kind,
702
+ accessibilityLabel: name,
703
+ accessibilityHint: hint,
704
+ accessibilityState: { ...accessibilityState, checked: state, disabled: isDisabled },
705
+ "aria-checked": state,
706
+ "aria-disabled": isDisabled,
707
+ disabled: isDisabled,
708
+ ...reactNative.Platform.OS === "web" ? {
709
+ "aria-describedby": descriptions,
710
+ "aria-required": isRequired,
711
+ "aria-invalid": isInvalid
712
+ } : {},
713
+ onPress: activate,
714
+ className: cn(
715
+ "min-h-[44px] min-w-[44px] flex-row items-center gap-3 rounded-[4px] py-2",
716
+ focusRing,
717
+ isDisabled && "opacity-50",
718
+ className
719
+ ),
720
+ children: [
721
+ kind === "checkbox" ? /* @__PURE__ */ jsxRuntime.jsx(
722
+ reactNative.View,
723
+ {
724
+ accessible: false,
725
+ importantForAccessibility: "no-hide-descendants",
726
+ className: cn(
727
+ "h-5 w-5 items-center justify-center rounded-[4px] border-2 border-ui-border",
728
+ (isChecked || mixed) && "border-bubba-red bg-bubba-red",
729
+ isInvalid && "border-ui-error"
730
+ ),
731
+ children: mixed ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { className: "h-0.5 w-3 bg-white" }) : isChecked ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { className: "h-[10px] w-[6px] -translate-y-px rotate-45 border-b-2 border-r-2 border-white" }) : null
732
+ }
733
+ ) : /* @__PURE__ */ jsxRuntime.jsx(
734
+ reactNative.View,
735
+ {
736
+ accessible: false,
737
+ importantForAccessibility: "no-hide-descendants",
738
+ className: cn(
739
+ "h-[26px] w-[44px] rounded-full border border-ui-border bg-ui-selected p-[2px]",
740
+ isChecked && "border-bubba-red bg-bubba-red",
741
+ isInvalid && "border-ui-error"
742
+ ),
743
+ children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { className: cn("h-5 w-5 rounded-full", isChecked ? "translate-x-[18px] bg-white" : "translate-x-0 bg-ui-muted") })
744
+ }
745
+ ),
746
+ label || description ? /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { className: "min-w-0 flex-1 gap-1", children: [
747
+ label ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { className: cn("text-base font-medium text-ui-text", textClassName), children: label }) : null,
748
+ description ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { nativeID: `${id}-description`, className: "text-sm leading-5 text-ui-muted", children: description }) : null
749
+ ] }) : null
750
+ ]
751
+ }
752
+ );
753
+ });
754
+ var Checkbox = react.forwardRef(function Checkbox2(props, ref) {
755
+ return /* @__PURE__ */ jsxRuntime.jsx(ChoiceControl, { ...props, ref, kind: "checkbox" });
756
+ });
757
+ var Switch = react.forwardRef(function Switch2(props, ref) {
758
+ return /* @__PURE__ */ jsxRuntime.jsx(ChoiceControl, { ...props, ref, kind: "switch" });
759
+ });
760
+ function useSelectionNavigation(items, value, onSelect, orientation, disabled = false, onActivate = onSelect) {
761
+ const refs = react.useRef([]);
762
+ const enabled = items.map((item, index) => !disabled && !item.disabled ? index : -1).filter((index) => index !== -1);
763
+ const selectedIndex = items.findIndex((item) => item.value === value && !item.disabled);
764
+ const tabStop = selectedIndex >= 0 && !disabled ? selectedIndex : enabled[0];
765
+ return (index) => ({
766
+ ref: (node) => {
767
+ refs.current[index] = node;
768
+ },
769
+ tabIndex: reactNative.Platform.OS === "web" && index !== tabStop ? -1 : 0,
770
+ ...reactNative.Platform.OS === "web" ? {
771
+ onKeyDown(event) {
772
+ if (disabled || items[index]?.disabled) return;
773
+ const key = event.key ?? event.nativeEvent?.key;
774
+ if (key === " " || key === "Spacebar") {
775
+ event.preventDefault();
776
+ if (!event.repeat) onActivate(items[index].value);
777
+ return;
778
+ }
779
+ const forward = key === (orientation === "horizontal" ? "ArrowRight" : "ArrowDown");
780
+ const backward = key === (orientation === "horizontal" ? "ArrowLeft" : "ArrowUp");
781
+ if (!forward && !backward && key !== "Home" && key !== "End") return;
782
+ event.preventDefault();
783
+ const position = enabled.indexOf(index);
784
+ const next = key === "Home" ? enabled[0] : key === "End" ? enabled[enabled.length - 1] : enabled[(position + (forward ? 1 : -1) + enabled.length) % enabled.length];
785
+ if (next === void 0) return;
786
+ onSelect(items[next].value);
787
+ refs.current[next]?.focus();
788
+ }
789
+ } : {}
790
+ });
791
+ }
792
+ var RadioGroup = react.forwardRef(function RadioGroup2({
793
+ options,
794
+ value,
795
+ defaultValue,
796
+ onValueChange,
797
+ label,
798
+ disabled = false,
799
+ required,
800
+ invalid,
801
+ orientation = "vertical",
802
+ className,
803
+ optionClassName,
804
+ textClassName,
805
+ nativeID,
806
+ accessibilityLabel,
807
+ accessibilityHint,
808
+ accessibilityState,
809
+ "aria-label": ariaLabel,
810
+ "aria-disabled": ariaDisabled,
811
+ ...props
812
+ }, ref) {
813
+ const field = useFormField();
814
+ const generatedID = react.useId().replace(/:/g, "");
815
+ const id = nativeID ?? field?.controlID ?? `bui-radio-${generatedID}`;
816
+ const isDisabled = Boolean(disabled || ariaDisabled || accessibilityState?.disabled || field?.disabled);
817
+ const isRequired = required ?? field?.required ?? false;
818
+ const isInvalid = invalid ?? field?.invalid ?? false;
819
+ const [selected, setSelected] = useControllableState({
820
+ value,
821
+ defaultValue,
822
+ onChange: (next) => {
823
+ if (next !== void 0) onValueChange?.(next);
824
+ }
825
+ });
826
+ const select = (next) => {
827
+ if (!isDisabled && options.some((option) => option.value === next && !option.disabled)) setSelected(next);
828
+ };
829
+ const navigation = useSelectionNavigation(options, selected, select, orientation, isDisabled);
830
+ const labelID = label ? `${id}-label` : field?.labelID;
831
+ const explicitLabel = accessibilityLabel ?? ariaLabel;
832
+ const name = explicitLabel ?? label ?? field?.label;
833
+ const hint = [
834
+ accessibilityHint ?? field?.hint,
835
+ field?.error && `Error: ${field.error}`,
836
+ isRequired && "Required"
837
+ ].filter(Boolean).join(". ") || void 0;
838
+ return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { ...props, ref, nativeID: id, className: cn("gap-2", className), children: [
839
+ label ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { nativeID: labelID, className: "text-base font-semibold text-ui-text", children: label }) : null,
840
+ /* @__PURE__ */ jsxRuntime.jsx(
841
+ reactNative.View,
842
+ {
843
+ accessibilityRole: "radiogroup",
844
+ accessibilityLabel: reactNative.Platform.OS === "web" && labelID && !explicitLabel ? void 0 : name,
845
+ accessibilityHint: hint,
846
+ accessibilityState: { ...accessibilityState, disabled: isDisabled },
847
+ "aria-disabled": isDisabled,
848
+ ...reactNative.Platform.OS === "web" ? {
849
+ "aria-labelledby": explicitLabel ? void 0 : labelID,
850
+ "aria-describedby": field?.descriptionID,
851
+ "aria-required": isRequired,
852
+ "aria-invalid": isInvalid,
853
+ "aria-orientation": orientation
854
+ } : {},
855
+ className: cn("gap-1", orientation === "horizontal" && "flex-row flex-wrap gap-x-6"),
856
+ children: options.map((option, index) => {
857
+ const checked = selected === option.value;
858
+ const optionDisabled = isDisabled || Boolean(option.disabled);
859
+ const optionID = `${id}-option-${index}`;
860
+ return /* @__PURE__ */ jsxRuntime.jsxs(
861
+ reactNative.Pressable,
862
+ {
863
+ ...navigation(index),
864
+ nativeID: optionID,
865
+ accessibilityRole: "radio",
866
+ accessibilityLabel: option.label,
867
+ accessibilityHint: option.description,
868
+ accessibilityState: { checked, disabled: optionDisabled },
869
+ "aria-checked": checked,
870
+ "aria-disabled": optionDisabled,
871
+ ...reactNative.Platform.OS === "web" ? { "aria-describedby": option.description ? `${optionID}-description` : void 0 } : {},
872
+ disabled: optionDisabled,
873
+ onPress: () => select(option.value),
874
+ className: cn(
875
+ "min-h-[44px] min-w-[44px] flex-row items-center gap-3 rounded-[4px] py-2",
876
+ focusRing,
877
+ optionDisabled && "opacity-50",
878
+ optionClassName
879
+ ),
880
+ children: [
881
+ /* @__PURE__ */ jsxRuntime.jsx(
882
+ reactNative.View,
883
+ {
884
+ accessible: false,
885
+ importantForAccessibility: "no-hide-descendants",
886
+ className: cn("h-5 w-5 items-center justify-center rounded-full border-2 border-ui-border", checked && "border-bubba-red"),
887
+ children: checked ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { className: "h-2.5 w-2.5 rounded-full bg-bubba-red" }) : null
888
+ }
889
+ ),
890
+ /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { className: "min-w-0 flex-1 gap-1", children: [
891
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { className: cn("text-base font-medium text-ui-text", textClassName), children: option.label }),
892
+ option.description ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { nativeID: `${optionID}-description`, className: "text-sm leading-5 text-ui-muted", children: option.description }) : null
893
+ ] })
894
+ ]
895
+ },
896
+ option.value
897
+ );
898
+ })
899
+ }
900
+ )
901
+ ] });
902
+ });
903
+ var Select = react.forwardRef(function Select2({
904
+ options,
905
+ value,
906
+ defaultValue,
907
+ onValueChange,
908
+ label,
909
+ placeholder = "Choose an option",
910
+ required,
911
+ invalid,
912
+ disabled,
913
+ className,
914
+ textClassName,
915
+ nativeID,
916
+ testID,
917
+ onPress,
918
+ accessibilityLabel,
919
+ accessibilityHint,
920
+ accessibilityState,
921
+ "aria-label": ariaLabel,
922
+ "aria-disabled": ariaDisabled,
923
+ ...props
924
+ }, ref) {
925
+ const field = useFormField();
926
+ const generatedID = react.useId().replace(/:/g, "");
927
+ const id = nativeID ?? field?.controlID ?? `bui-select-${generatedID}`;
928
+ const isDisabled = Boolean(disabled || ariaDisabled || accessibilityState?.disabled || field?.disabled);
929
+ const isInvalid = invalid ?? field?.invalid ?? false;
930
+ const isRequired = required ?? field?.required ?? false;
931
+ const [selected, setSelected] = useControllableState({
932
+ value,
933
+ defaultValue,
934
+ onChange: (next) => {
935
+ if (next !== void 0) onValueChange?.(next);
936
+ }
937
+ });
938
+ const [open, setOpen] = react.useState(false);
939
+ const [pending, setPending] = react.useState(selected);
940
+ react.useEffect(() => {
941
+ if (isDisabled) setOpen(false);
942
+ }, [isDisabled]);
943
+ const selectedOption = options.find((option) => option.value === selected);
944
+ const title = label ?? field?.label ?? "Choose an option";
945
+ const labelID = label ? `${id}-label` : field?.labelID;
946
+ const explicitLabel = accessibilityLabel ?? ariaLabel;
947
+ const hint = [
948
+ accessibilityHint ?? field?.hint,
949
+ field?.error && `Error: ${field.error}`,
950
+ isRequired && "Required",
951
+ "Opens a list of choices"
952
+ ].filter(Boolean).join(". ");
953
+ const commit = (next) => {
954
+ if (isDisabled || !options.some((option) => option.value === next && !option.disabled)) return;
955
+ setSelected(next);
956
+ setOpen(false);
957
+ };
958
+ const navigation = useSelectionNavigation(options, pending, setPending, "vertical", isDisabled, commit);
959
+ return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { className: "gap-2", children: [
960
+ label ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { nativeID: labelID, className: "text-base font-semibold text-ui-text", children: label }) : null,
961
+ /* @__PURE__ */ jsxRuntime.jsxs(
962
+ reactNative.Pressable,
963
+ {
964
+ ...props,
965
+ ref,
966
+ nativeID: id,
967
+ testID,
968
+ accessibilityRole: "button",
969
+ accessibilityLabel: explicitLabel ?? (reactNative.Platform.OS === "web" ? void 0 : `${title}: ${selectedOption?.label ?? placeholder}`),
970
+ accessibilityHint: hint,
971
+ accessibilityState: { ...accessibilityState, disabled: isDisabled, expanded: open },
972
+ "aria-disabled": isDisabled,
973
+ "aria-expanded": open,
974
+ disabled: isDisabled,
975
+ ...reactNative.Platform.OS === "web" ? {
976
+ "aria-haspopup": "dialog",
977
+ "aria-labelledby": !explicitLabel && labelID ? `${labelID} ${id}-value` : void 0,
978
+ "aria-describedby": field?.descriptionID,
979
+ "aria-invalid": isInvalid
980
+ } : {},
981
+ onPress: (event) => {
982
+ if (!isDisabled) {
983
+ setPending(selected);
984
+ setOpen(true);
985
+ }
986
+ onPress?.(event);
987
+ },
988
+ className: cn(
989
+ "min-h-[48px] flex-row items-center justify-between gap-4 rounded-[4px] border border-ui-border bg-ui-surface px-3 py-3",
990
+ focusRing,
991
+ isInvalid && "border-ui-error",
992
+ isDisabled && "bg-ui-disabled-surface",
993
+ className
994
+ ),
995
+ children: [
996
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { nativeID: `${id}-value`, className: cn(
997
+ "min-w-0 flex-1 text-base",
998
+ selectedOption ? "text-ui-text" : "text-ui-muted",
999
+ isDisabled && "text-ui-disabled-text",
1000
+ textClassName
1001
+ ), children: selectedOption?.label ?? placeholder }),
1002
+ /* @__PURE__ */ jsxRuntime.jsx(
1003
+ reactNative.View,
1004
+ {
1005
+ accessible: false,
1006
+ importantForAccessibility: "no-hide-descendants",
1007
+ className: "mr-1 h-2 w-2 rotate-45 border-b-2 border-r-2 border-ui-muted"
1008
+ }
1009
+ )
1010
+ ]
1011
+ }
1012
+ ),
1013
+ /* @__PURE__ */ jsxRuntime.jsx(
1014
+ Dialog,
1015
+ {
1016
+ open,
1017
+ onOpenChange: setOpen,
1018
+ title,
1019
+ closeLabel: "Cancel",
1020
+ testID: testID ? `${testID}-dialog` : void 0,
1021
+ children: /* @__PURE__ */ jsxRuntime.jsxs(
1022
+ reactNative.View,
1023
+ {
1024
+ accessibilityRole: "radiogroup",
1025
+ accessibilityLabel: title,
1026
+ ...reactNative.Platform.OS === "web" ? { "aria-required": isRequired } : {},
1027
+ className: "gap-1",
1028
+ children: [
1029
+ options.map((option, index) => {
1030
+ const checked = pending === option.value;
1031
+ const optionID = `${id}-option-${index}`;
1032
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1033
+ reactNative.Pressable,
1034
+ {
1035
+ ...navigation(index),
1036
+ nativeID: optionID,
1037
+ testID: testID ? `${testID}-option-${option.value}` : void 0,
1038
+ accessibilityRole: "radio",
1039
+ accessibilityLabel: option.label,
1040
+ accessibilityHint: option.description,
1041
+ accessibilityState: { checked, disabled: Boolean(option.disabled) },
1042
+ "aria-checked": checked,
1043
+ "aria-disabled": Boolean(option.disabled),
1044
+ disabled: option.disabled,
1045
+ ...reactNative.Platform.OS === "web" ? { "aria-describedby": option.description ? `${optionID}-description` : void 0 } : {},
1046
+ onPress: () => commit(option.value),
1047
+ className: cn(
1048
+ "min-h-[48px] min-w-[44px] flex-row items-center gap-3 rounded-[4px] px-3 py-3",
1049
+ focusRing,
1050
+ checked && "bg-ui-selected",
1051
+ option.disabled && "opacity-50"
1052
+ ),
1053
+ children: [
1054
+ /* @__PURE__ */ jsxRuntime.jsx(
1055
+ reactNative.View,
1056
+ {
1057
+ accessible: false,
1058
+ importantForAccessibility: "no-hide-descendants",
1059
+ className: cn("h-5 w-5 items-center justify-center rounded-full border-2 border-ui-border", checked && "border-bubba-red"),
1060
+ children: checked ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { className: "h-2.5 w-2.5 rounded-full bg-bubba-red" }) : null
1061
+ }
1062
+ ),
1063
+ /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { className: "min-w-0 flex-1 gap-1", children: [
1064
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { className: "text-base font-medium text-ui-text", children: option.label }),
1065
+ option.description ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { nativeID: `${optionID}-description`, className: "text-sm leading-5 text-ui-muted", children: option.description }) : null
1066
+ ] })
1067
+ ]
1068
+ },
1069
+ option.value
1070
+ );
1071
+ }),
1072
+ options.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { className: "px-3 py-4 text-base text-ui-muted", children: "No options available." }) : null
1073
+ ]
1074
+ }
1075
+ )
1076
+ }
1077
+ )
1078
+ ] });
1079
+ });
1080
+ var Tabs = react.forwardRef(function Tabs2({
1081
+ items,
1082
+ value,
1083
+ defaultValue,
1084
+ onValueChange,
1085
+ label = "Content sections",
1086
+ orientation = "horizontal",
1087
+ className,
1088
+ tabListClassName,
1089
+ tabClassName,
1090
+ textClassName,
1091
+ panelClassName,
1092
+ nativeID,
1093
+ ...props
1094
+ }, ref) {
1095
+ const generatedID = react.useId().replace(/:/g, "");
1096
+ const id = nativeID ?? `bui-tabs-${generatedID}`;
1097
+ const [selected, setSelected] = useControllableState({
1098
+ value,
1099
+ defaultValue: defaultValue ?? items.find((item) => !item.disabled)?.value,
1100
+ onChange: (next) => {
1101
+ if (next !== void 0) onValueChange?.(next);
1102
+ }
1103
+ });
1104
+ const select = (next) => {
1105
+ if (items.some((item) => item.value === next && !item.disabled)) setSelected(next);
1106
+ };
1107
+ const navigation = useSelectionNavigation(items, selected, select, orientation);
1108
+ const activeIndex = items.findIndex((item) => item.value === selected && !item.disabled);
1109
+ const active = items[activeIndex];
1110
+ return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { ...props, ref, nativeID: id, className: cn("gap-4", className), children: [
1111
+ /* @__PURE__ */ jsxRuntime.jsx(
1112
+ reactNative.View,
1113
+ {
1114
+ accessibilityRole: "tablist",
1115
+ accessibilityLabel: label,
1116
+ ...reactNative.Platform.OS === "web" ? { "aria-orientation": orientation } : {},
1117
+ className: cn(
1118
+ "gap-1 rounded-md border border-ui-border bg-ui-selected p-1",
1119
+ orientation === "horizontal" && "flex-row flex-wrap",
1120
+ tabListClassName
1121
+ ),
1122
+ children: items.map((item, index) => {
1123
+ const isSelected = index === activeIndex;
1124
+ return /* @__PURE__ */ jsxRuntime.jsx(
1125
+ reactNative.Pressable,
1126
+ {
1127
+ ...navigation(index),
1128
+ nativeID: `${id}-tab-${index}`,
1129
+ accessibilityRole: "tab",
1130
+ accessibilityLabel: item.label,
1131
+ accessibilityState: { selected: isSelected, disabled: Boolean(item.disabled) },
1132
+ "aria-selected": isSelected,
1133
+ "aria-disabled": Boolean(item.disabled),
1134
+ disabled: item.disabled,
1135
+ ...reactNative.Platform.OS === "web" ? { "aria-controls": isSelected ? `${id}-panel-${index}` : void 0 } : {},
1136
+ onPress: () => select(item.value),
1137
+ className: cn(
1138
+ "min-h-[44px] min-w-[44px] items-center justify-center rounded-[4px] px-4 py-2",
1139
+ focusRing,
1140
+ isSelected && "bg-ui-surface",
1141
+ item.disabled && "opacity-50",
1142
+ tabClassName
1143
+ ),
1144
+ children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { className: cn("text-base font-semibold", isSelected ? "text-ui-text" : "text-ui-muted", textClassName), children: item.label })
1145
+ },
1146
+ item.value
1147
+ );
1148
+ })
1149
+ }
1150
+ ),
1151
+ active ? /* @__PURE__ */ jsxRuntime.jsx(
1152
+ reactNative.View,
1153
+ {
1154
+ nativeID: `${id}-panel-${activeIndex}`,
1155
+ role: "tabpanel",
1156
+ accessibilityLabel: reactNative.Platform.OS === "web" ? void 0 : active.label,
1157
+ tabIndex: 0,
1158
+ ...reactNative.Platform.OS === "web" ? { "aria-labelledby": `${id}-tab-${activeIndex}` } : {},
1159
+ className: cn("rounded-[4px]", focusRing, panelClassName),
1160
+ children: active.content
1161
+ },
1162
+ active.value
1163
+ ) : null
1164
+ ] });
1165
+ });
108
1166
 
1167
+ exports.Accordion = Accordion;
1168
+ exports.Alert = Alert;
1169
+ exports.Avatar = Avatar;
1170
+ exports.Badge = Badge;
1171
+ exports.Breadcrumbs = Breadcrumbs;
109
1172
  exports.Button = Button;
1173
+ exports.Card = Card;
1174
+ exports.Checkbox = Checkbox;
1175
+ exports.Container = Container;
1176
+ exports.Dialog = Dialog;
1177
+ exports.Divider = Divider;
1178
+ exports.EmptyState = EmptyState;
1179
+ exports.FormField = FormField;
1180
+ exports.Heading = Heading;
1181
+ exports.IconButton = IconButton;
1182
+ exports.Inline = Inline;
1183
+ exports.Input = Input;
1184
+ exports.Link = Link;
1185
+ exports.ListItem = ListItem;
1186
+ exports.Pagination = Pagination;
1187
+ exports.Progress = Progress;
1188
+ exports.RadioGroup = RadioGroup;
1189
+ exports.Select = Select;
1190
+ exports.Sheet = Sheet;
1191
+ exports.Skeleton = Skeleton;
1192
+ exports.Spinner = Spinner;
1193
+ exports.Stack = Stack;
1194
+ exports.Switch = Switch;
1195
+ exports.Tabs = Tabs;
1196
+ exports.Text = Text2;
1197
+ exports.Textarea = Textarea;
1198
+ exports.ThemeProvider = ThemeProvider;
1199
+ exports.Toast = Toast;
1200
+ exports.themeVariables = themeVariables;
110
1201
  exports.tokens = tokens;
1202
+ exports.useTheme = useTheme;