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