@bubbasfw/ui 0.0.0-stage → 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Howeth and Harp
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,72 @@
1
- # Temporary Holding Version
1
+ # @bubbasfw/ui
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Bubba’s universal React Native design system scaffold. The same NativeWind
4
+ Button uses `Pressable`, `Text`, and `ActivityIndicator` on native platforms and
5
+ React Native Web. This initial package contains the Button and foundation tokens.
6
+
7
+ ```tsx
8
+ import { Button } from '@bubbasfw/ui';
9
+
10
+ <Button onPress={save} loading={saving} loadingLabel="Saving…">
11
+ Save changes
12
+ </Button>
13
+ ```
14
+
15
+ Variants: `primary`, `secondary`, `outline`, `ghost`. Sizes: `sm`, `md`, `lg`.
16
+ Use `className` for the Pressable and `textClassName` for the label. Native
17
+ Pressable props and refs are forwarded. Loading and disabled states block presses
18
+ and expose accessibility state. Label content should be text or inline native Text.
19
+
20
+ ## Consumer setup
21
+
22
+ Baseline: React 19.2.3, React Native 0.86.3, React Native Web 0.21.4,
23
+ NativeWind 4.2.7, Tailwind 3.4.17. Configure the host app for NativeWind and scan
24
+ this package’s source so its utilities are generated:
25
+
26
+ ```sh
27
+ pnpm add @bubbasfw/ui nativewind@4.2.7
28
+ pnpm add -D tailwindcss@3.4.17
29
+ ```
30
+
31
+ Expo apps must also declare NativeWind 4's Babel runtime directly:
32
+
33
+ ```sh
34
+ pnpm add react-native-css-interop@0.2.7
35
+ npx expo install react-native-reanimated react-native-worklets react-native-safe-area-context
36
+ ```
37
+
38
+ The Babel preset emits `react-native-css-interop` imports into app files. Its
39
+ transitive dependency through NativeWind does not resolve from those files in
40
+ pnpm's isolated dependency layout.
41
+
42
+ ```js
43
+ const path = require('node:path');
44
+ const uiRoot = path.dirname(require.resolve('@bubbasfw/ui/package.json'));
45
+
46
+ module.exports = {
47
+ content: [
48
+ './App.{js,jsx,ts,tsx}',
49
+ './app/**/*.{js,jsx,ts,tsx}',
50
+ './pages/**/*.{js,jsx,ts,tsx}',
51
+ path.join(uiRoot, 'src/**/*.{ts,tsx}'),
52
+ ],
53
+ presets: [
54
+ require('nativewind/preset'),
55
+ require('@bubbasfw/ui/tailwind-preset'),
56
+ ],
57
+ };
58
+ ```
59
+
60
+ Expo requires NativeWind’s Metro/Babel setup and compatible native dependencies.
61
+ Next.js requires React Native Web aliasing, NativeWind’s JSX import source,
62
+ Tailwind 3/PostCSS, and runtime transpilation. App Router interactive consumers
63
+ need a client boundary. See the repository’s
64
+ [complete setup and runnable examples](https://github.com/handh-dev/bubbas-ui#readme).
65
+
66
+ The package exports `@bubbasfw/ui/tokens` without React runtime imports and
67
+ `@bubbasfw/ui/tailwind-preset` for shared color/radius utilities. Platform peers
68
+ are external; React Native Web is optional for native-only consumers.
69
+
70
+ Published releases use public npm access. The code is licensed under
71
+ [MIT](LICENSE). Font and artwork binaries are excluded and retain
72
+ their separate distribution rights. No trademark license is granted.
package/dist/index.cjs ADDED
@@ -0,0 +1,110 @@
1
+ "use client";
2
+ 'use strict';
3
+
4
+ var react = require('react');
5
+ var reactNative = require('react-native');
6
+ var tailwindMerge = require('tailwind-merge');
7
+ var jsxRuntime = require('nativewind/jsx-runtime');
8
+
9
+ // src/tokens.ts
10
+ var tokens = {
11
+ brandVersion: "1.0.0",
12
+ colors: {
13
+ blue: "#007CE8",
14
+ red: "#CC0000",
15
+ redHover: "#A80000",
16
+ navy: "#03256C",
17
+ night: "#001845",
18
+ ink: "#131B23",
19
+ muted: "#526075",
20
+ line: "#D9E8FB",
21
+ white: "#FFFFFF",
22
+ gold: "#FED766",
23
+ disabled: "#F0F2F5",
24
+ disabledText: "#6B727E"
25
+ },
26
+ spacing: { 1: 4, 2: 8, 3: 12, 4: 16, 6: 24, 8: 32, 12: 48, 16: 64 },
27
+ radius: { control: 4, panel: 12 },
28
+ touchTarget: { minimum: 44, default: 48 }
29
+ };
30
+ 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",
34
+ ghost: "border-transparent bg-transparent"
35
+ };
36
+ var textVariants = {
37
+ primary: "text-bubba-white",
38
+ secondary: "text-bubba-white",
39
+ outline: "text-bubba-navy",
40
+ ghost: "text-bubba-navy"
41
+ };
42
+ var containerSizes = {
43
+ sm: "min-h-[44px] px-3 py-2",
44
+ md: "min-h-[48px] px-5 py-3",
45
+ lg: "min-h-[56px] px-6 py-4"
46
+ };
47
+ var textSizes = {
48
+ sm: "text-sm",
49
+ md: "text-base",
50
+ lg: "text-lg"
51
+ };
52
+ var Button = react.forwardRef(
53
+ function Button2({
54
+ children,
55
+ variant = "primary",
56
+ size = "md",
57
+ loading = false,
58
+ loadingLabel,
59
+ disabled = false,
60
+ className,
61
+ textClassName,
62
+ accessibilityState,
63
+ "aria-busy": ariaBusy,
64
+ "aria-disabled": ariaDisabled,
65
+ ...props
66
+ }, ref) {
67
+ const explicitlyDisabled = disabled || (ariaDisabled ?? accessibilityState?.disabled ?? false);
68
+ const isDisabled = explicitlyDisabled || loading;
69
+ const isBusy = loading || (ariaBusy ?? accessibilityState?.busy ?? false);
70
+ const spinnerColor = explicitlyDisabled ? tokens.colors.disabledText : variant === "primary" || variant === "secondary" ? tokens.colors.white : tokens.colors.navy;
71
+ return /* @__PURE__ */ jsxRuntime.jsxs(
72
+ reactNative.Pressable,
73
+ {
74
+ ...props,
75
+ ref,
76
+ accessibilityRole: "button",
77
+ accessibilityState: { ...accessibilityState, disabled: isDisabled, busy: isBusy },
78
+ "aria-busy": isBusy,
79
+ "aria-disabled": isDisabled,
80
+ 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",
83
+ containerVariants[variant],
84
+ containerSizes[size],
85
+ explicitlyDisabled && "border-bubba-disabled bg-bubba-disabled",
86
+ className
87
+ ),
88
+ children: [
89
+ loading ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.ActivityIndicator, { size: "small", color: spinnerColor, accessible: false }) : null,
90
+ /* @__PURE__ */ jsxRuntime.jsx(
91
+ reactNative.Text,
92
+ {
93
+ className: tailwindMerge.twMerge(
94
+ "font-bold text-center",
95
+ textVariants[variant],
96
+ textSizes[size],
97
+ explicitlyDisabled && "text-bubba-disabledText",
98
+ textClassName
99
+ ),
100
+ children: loading && loadingLabel ? loadingLabel : children
101
+ }
102
+ )
103
+ ]
104
+ }
105
+ );
106
+ }
107
+ );
108
+
109
+ exports.Button = Button;
110
+ exports.tokens = tokens;
@@ -0,0 +1,22 @@
1
+ import * as react from 'react';
2
+ import { ReactNode } from 'react';
3
+ import * as react_native from 'react-native';
4
+ import { PressableProps } from 'react-native';
5
+ export { BubbaTokens, tokens } from './tokens.cjs';
6
+
7
+ type ButtonVariant = 'primary' | 'secondary' | 'outline' | 'ghost';
8
+ type ButtonSize = 'sm' | 'md' | 'lg';
9
+ interface ButtonProps extends Omit<PressableProps, 'children' | 'className'> {
10
+ /** Text content, or inline React Native Text content. */
11
+ children: ReactNode;
12
+ variant?: ButtonVariant;
13
+ size?: ButtonSize;
14
+ loading?: boolean;
15
+ loadingLabel?: string;
16
+ className?: string;
17
+ textClassName?: string;
18
+ }
19
+ /** One native Pressable and Text implementation on iOS, Android, and web. */
20
+ declare const Button: react.ForwardRefExoticComponent<ButtonProps & react.RefAttributes<react_native.View>>;
21
+
22
+ export { Button, type ButtonProps, type ButtonSize, type ButtonVariant };
@@ -0,0 +1,22 @@
1
+ import * as react from 'react';
2
+ import { ReactNode } from 'react';
3
+ import * as react_native from 'react-native';
4
+ import { PressableProps } from 'react-native';
5
+ export { BubbaTokens, tokens } from './tokens.js';
6
+
7
+ type ButtonVariant = 'primary' | 'secondary' | 'outline' | 'ghost';
8
+ type ButtonSize = 'sm' | 'md' | 'lg';
9
+ interface ButtonProps extends Omit<PressableProps, 'children' | 'className'> {
10
+ /** Text content, or inline React Native Text content. */
11
+ children: ReactNode;
12
+ variant?: ButtonVariant;
13
+ size?: ButtonSize;
14
+ loading?: boolean;
15
+ loadingLabel?: string;
16
+ className?: string;
17
+ textClassName?: string;
18
+ }
19
+ /** One native Pressable and Text implementation on iOS, Android, and web. */
20
+ declare const Button: react.ForwardRefExoticComponent<ButtonProps & react.RefAttributes<react_native.View>>;
21
+
22
+ export { Button, type ButtonProps, type ButtonSize, type ButtonVariant };
package/dist/index.js ADDED
@@ -0,0 +1,107 @@
1
+ "use client";
2
+ import { forwardRef } from 'react';
3
+ import { Pressable, ActivityIndicator, Text } from 'react-native';
4
+ import { twMerge } from 'tailwind-merge';
5
+ import { jsxs, jsx } from 'nativewind/jsx-runtime';
6
+
7
+ // src/tokens.ts
8
+ var tokens = {
9
+ brandVersion: "1.0.0",
10
+ colors: {
11
+ blue: "#007CE8",
12
+ red: "#CC0000",
13
+ redHover: "#A80000",
14
+ navy: "#03256C",
15
+ night: "#001845",
16
+ ink: "#131B23",
17
+ muted: "#526075",
18
+ line: "#D9E8FB",
19
+ white: "#FFFFFF",
20
+ gold: "#FED766",
21
+ disabled: "#F0F2F5",
22
+ disabledText: "#6B727E"
23
+ },
24
+ spacing: { 1: 4, 2: 8, 3: 12, 4: 16, 6: 24, 8: 32, 12: 48, 16: 64 },
25
+ radius: { control: 4, panel: 12 },
26
+ touchTarget: { minimum: 44, default: 48 }
27
+ };
28
+ 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",
32
+ ghost: "border-transparent bg-transparent"
33
+ };
34
+ var textVariants = {
35
+ primary: "text-bubba-white",
36
+ secondary: "text-bubba-white",
37
+ outline: "text-bubba-navy",
38
+ ghost: "text-bubba-navy"
39
+ };
40
+ var containerSizes = {
41
+ sm: "min-h-[44px] px-3 py-2",
42
+ md: "min-h-[48px] px-5 py-3",
43
+ lg: "min-h-[56px] px-6 py-4"
44
+ };
45
+ var textSizes = {
46
+ sm: "text-sm",
47
+ md: "text-base",
48
+ lg: "text-lg"
49
+ };
50
+ var Button = forwardRef(
51
+ function Button2({
52
+ children,
53
+ variant = "primary",
54
+ size = "md",
55
+ loading = false,
56
+ loadingLabel,
57
+ disabled = false,
58
+ className,
59
+ textClassName,
60
+ accessibilityState,
61
+ "aria-busy": ariaBusy,
62
+ "aria-disabled": ariaDisabled,
63
+ ...props
64
+ }, ref) {
65
+ const explicitlyDisabled = disabled || (ariaDisabled ?? accessibilityState?.disabled ?? false);
66
+ const isDisabled = explicitlyDisabled || loading;
67
+ const isBusy = loading || (ariaBusy ?? accessibilityState?.busy ?? false);
68
+ const spinnerColor = explicitlyDisabled ? tokens.colors.disabledText : variant === "primary" || variant === "secondary" ? tokens.colors.white : tokens.colors.navy;
69
+ return /* @__PURE__ */ jsxs(
70
+ Pressable,
71
+ {
72
+ ...props,
73
+ ref,
74
+ accessibilityRole: "button",
75
+ accessibilityState: { ...accessibilityState, disabled: isDisabled, busy: isBusy },
76
+ "aria-busy": isBusy,
77
+ "aria-disabled": isDisabled,
78
+ 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",
81
+ containerVariants[variant],
82
+ containerSizes[size],
83
+ explicitlyDisabled && "border-bubba-disabled bg-bubba-disabled",
84
+ className
85
+ ),
86
+ children: [
87
+ loading ? /* @__PURE__ */ jsx(ActivityIndicator, { size: "small", color: spinnerColor, accessible: false }) : null,
88
+ /* @__PURE__ */ jsx(
89
+ Text,
90
+ {
91
+ className: twMerge(
92
+ "font-bold text-center",
93
+ textVariants[variant],
94
+ textSizes[size],
95
+ explicitlyDisabled && "text-bubba-disabledText",
96
+ textClassName
97
+ ),
98
+ children: loading && loadingLabel ? loadingLabel : children
99
+ }
100
+ )
101
+ ]
102
+ }
103
+ );
104
+ }
105
+ );
106
+
107
+ export { Button, tokens };
@@ -0,0 +1,25 @@
1
+ 'use strict';
2
+
3
+ // src/tokens.ts
4
+ var tokens = {
5
+ brandVersion: "1.0.0",
6
+ colors: {
7
+ blue: "#007CE8",
8
+ red: "#CC0000",
9
+ redHover: "#A80000",
10
+ navy: "#03256C",
11
+ night: "#001845",
12
+ ink: "#131B23",
13
+ muted: "#526075",
14
+ line: "#D9E8FB",
15
+ white: "#FFFFFF",
16
+ gold: "#FED766",
17
+ disabled: "#F0F2F5",
18
+ disabledText: "#6B727E"
19
+ },
20
+ spacing: { 1: 4, 2: 8, 3: 12, 4: 16, 6: 24, 8: 32, 12: 48, 16: 64 },
21
+ radius: { control: 4, panel: 12 },
22
+ touchTarget: { minimum: 44, default: 48 }
23
+ };
24
+
25
+ exports.tokens = tokens;
@@ -0,0 +1,39 @@
1
+ /** Pinned foundations from Bubba's Brand Studio brand release 1.0.0. */
2
+ declare const tokens: {
3
+ readonly brandVersion: "1.0.0";
4
+ readonly colors: {
5
+ readonly blue: "#007CE8";
6
+ readonly red: "#CC0000";
7
+ readonly redHover: "#A80000";
8
+ readonly navy: "#03256C";
9
+ readonly night: "#001845";
10
+ readonly ink: "#131B23";
11
+ readonly muted: "#526075";
12
+ readonly line: "#D9E8FB";
13
+ readonly white: "#FFFFFF";
14
+ readonly gold: "#FED766";
15
+ readonly disabled: "#F0F2F5";
16
+ readonly disabledText: "#6B727E";
17
+ };
18
+ readonly spacing: {
19
+ readonly 1: 4;
20
+ readonly 2: 8;
21
+ readonly 3: 12;
22
+ readonly 4: 16;
23
+ readonly 6: 24;
24
+ readonly 8: 32;
25
+ readonly 12: 48;
26
+ readonly 16: 64;
27
+ };
28
+ readonly radius: {
29
+ readonly control: 4;
30
+ readonly panel: 12;
31
+ };
32
+ readonly touchTarget: {
33
+ readonly minimum: 44;
34
+ readonly default: 48;
35
+ };
36
+ };
37
+ type BubbaTokens = typeof tokens;
38
+
39
+ export { type BubbaTokens, tokens };
@@ -0,0 +1,39 @@
1
+ /** Pinned foundations from Bubba's Brand Studio brand release 1.0.0. */
2
+ declare const tokens: {
3
+ readonly brandVersion: "1.0.0";
4
+ readonly colors: {
5
+ readonly blue: "#007CE8";
6
+ readonly red: "#CC0000";
7
+ readonly redHover: "#A80000";
8
+ readonly navy: "#03256C";
9
+ readonly night: "#001845";
10
+ readonly ink: "#131B23";
11
+ readonly muted: "#526075";
12
+ readonly line: "#D9E8FB";
13
+ readonly white: "#FFFFFF";
14
+ readonly gold: "#FED766";
15
+ readonly disabled: "#F0F2F5";
16
+ readonly disabledText: "#6B727E";
17
+ };
18
+ readonly spacing: {
19
+ readonly 1: 4;
20
+ readonly 2: 8;
21
+ readonly 3: 12;
22
+ readonly 4: 16;
23
+ readonly 6: 24;
24
+ readonly 8: 32;
25
+ readonly 12: 48;
26
+ readonly 16: 64;
27
+ };
28
+ readonly radius: {
29
+ readonly control: 4;
30
+ readonly panel: 12;
31
+ };
32
+ readonly touchTarget: {
33
+ readonly minimum: 44;
34
+ readonly default: 48;
35
+ };
36
+ };
37
+ type BubbaTokens = typeof tokens;
38
+
39
+ export { type BubbaTokens, tokens };
package/dist/tokens.js ADDED
@@ -0,0 +1,23 @@
1
+ // src/tokens.ts
2
+ var tokens = {
3
+ brandVersion: "1.0.0",
4
+ colors: {
5
+ blue: "#007CE8",
6
+ red: "#CC0000",
7
+ redHover: "#A80000",
8
+ navy: "#03256C",
9
+ night: "#001845",
10
+ ink: "#131B23",
11
+ muted: "#526075",
12
+ line: "#D9E8FB",
13
+ white: "#FFFFFF",
14
+ gold: "#FED766",
15
+ disabled: "#F0F2F5",
16
+ disabledText: "#6B727E"
17
+ },
18
+ spacing: { 1: 4, 2: 8, 3: 12, 4: 16, 6: 24, 8: 32, 12: 48, 16: 64 },
19
+ radius: { control: 4, panel: 12 },
20
+ touchTarget: { minimum: 44, default: 48 }
21
+ };
22
+
23
+ export { tokens };
package/package.json CHANGED
@@ -1,6 +1,87 @@
1
1
  {
2
2
  "name": "@bubbasfw/ui",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "Bubba\u2019s universal React Native and web design system, styled with NativeWind.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "main": "./dist/index.cjs",
8
+ "module": "./dist/index.js",
9
+ "react-native": "./dist/index.js",
10
+ "types": "./dist/index.d.ts",
11
+ "exports": {
12
+ ".": {
13
+ "react-native": {
14
+ "types": "./dist/index.d.ts",
15
+ "default": "./dist/index.js"
16
+ },
17
+ "import": {
18
+ "types": "./dist/index.d.ts",
19
+ "default": "./dist/index.js"
20
+ },
21
+ "require": {
22
+ "types": "./dist/index.d.cts",
23
+ "default": "./dist/index.cjs"
24
+ },
25
+ "default": "./dist/index.js"
26
+ },
27
+ "./tokens": {
28
+ "import": {
29
+ "types": "./dist/tokens.d.ts",
30
+ "default": "./dist/tokens.js"
31
+ },
32
+ "require": {
33
+ "types": "./dist/tokens.d.cts",
34
+ "default": "./dist/tokens.cjs"
35
+ },
36
+ "default": "./dist/tokens.js"
37
+ },
38
+ "./tailwind-preset": "./tailwind-preset.cjs",
39
+ "./package.json": "./package.json"
40
+ },
41
+ "files": [
42
+ "dist",
43
+ "src",
44
+ "tailwind-preset.cjs",
45
+ "README.md",
46
+ "LICENSE"
47
+ ],
48
+ "sideEffects": false,
49
+ "publishConfig": {
50
+ "access": "public"
51
+ },
52
+ "repository": {
53
+ "type": "git",
54
+ "url": "https://github.com/handh-dev/bubbas-ui.git",
55
+ "directory": "packages/ui"
56
+ },
57
+ "homepage": "https://github.com/handh-dev/bubbas-ui#readme",
58
+ "keywords": [
59
+ "react-native",
60
+ "react-native-web",
61
+ "expo",
62
+ "nextjs",
63
+ "nativewind",
64
+ "design-system",
65
+ "bubbas"
66
+ ],
67
+ "scripts": {
68
+ "build": "tsup",
69
+ "typecheck": "tsc --noEmit",
70
+ "prepack": "pnpm build"
71
+ },
72
+ "dependencies": {
73
+ "tailwind-merge": "^2.6.1"
74
+ },
75
+ "peerDependencies": {
76
+ "react": "^19.2.3",
77
+ "react-native": "^0.86.3",
78
+ "react-native-web": "^0.21.4",
79
+ "nativewind": "^4.2.7",
80
+ "tailwindcss": "^3.4.17"
81
+ },
82
+ "peerDependenciesMeta": {
83
+ "react-native-web": {
84
+ "optional": true
85
+ }
86
+ }
87
+ }
@@ -0,0 +1,105 @@
1
+ 'use client';
2
+
3
+ import { forwardRef, type ElementRef, type ReactNode } from 'react';
4
+ import { ActivityIndicator, Pressable, Text, type PressableProps } from 'react-native';
5
+ import { twMerge } from 'tailwind-merge';
6
+ import { tokens } from '../tokens';
7
+
8
+ export type ButtonVariant = 'primary' | 'secondary' | 'outline' | 'ghost';
9
+ export type ButtonSize = 'sm' | 'md' | 'lg';
10
+
11
+ export interface ButtonProps extends Omit<PressableProps, 'children' | 'className'> {
12
+ /** Text content, or inline React Native Text content. */
13
+ children: ReactNode;
14
+ variant?: ButtonVariant;
15
+ size?: ButtonSize;
16
+ loading?: boolean;
17
+ loadingLabel?: string;
18
+ className?: string;
19
+ textClassName?: string;
20
+ }
21
+
22
+ const containerVariants: Record<ButtonVariant, string> = {
23
+ primary: 'border-bubba-red bg-bubba-red',
24
+ secondary: 'border-bubba-navy bg-bubba-navy',
25
+ outline: 'border-bubba-navy bg-transparent',
26
+ ghost: 'border-transparent bg-transparent',
27
+ };
28
+ const textVariants: Record<ButtonVariant, string> = {
29
+ primary: 'text-bubba-white',
30
+ secondary: 'text-bubba-white',
31
+ outline: 'text-bubba-navy',
32
+ ghost: 'text-bubba-navy',
33
+ };
34
+ const containerSizes: Record<ButtonSize, string> = {
35
+ sm: 'min-h-[44px] px-3 py-2',
36
+ md: 'min-h-[48px] px-5 py-3',
37
+ lg: 'min-h-[56px] px-6 py-4',
38
+ };
39
+ const textSizes: Record<ButtonSize, string> = {
40
+ sm: 'text-sm',
41
+ md: 'text-base',
42
+ lg: 'text-lg',
43
+ };
44
+
45
+ /** One native Pressable and Text implementation on iOS, Android, and web. */
46
+ export const Button = forwardRef<ElementRef<typeof Pressable>, ButtonProps>(
47
+ function Button(
48
+ {
49
+ children,
50
+ variant = 'primary',
51
+ size = 'md',
52
+ loading = false,
53
+ loadingLabel,
54
+ disabled = false,
55
+ className,
56
+ textClassName,
57
+ accessibilityState,
58
+ 'aria-busy': ariaBusy,
59
+ 'aria-disabled': ariaDisabled,
60
+ ...props
61
+ },
62
+ ref,
63
+ ) {
64
+ const explicitlyDisabled = disabled || (ariaDisabled ?? accessibilityState?.disabled ?? false);
65
+ const isDisabled = explicitlyDisabled || loading;
66
+ const isBusy = loading || (ariaBusy ?? accessibilityState?.busy ?? false);
67
+ const spinnerColor = explicitlyDisabled
68
+ ? tokens.colors.disabledText
69
+ : variant === 'primary' || variant === 'secondary'
70
+ ? tokens.colors.white
71
+ : tokens.colors.navy;
72
+
73
+ return (
74
+ <Pressable
75
+ {...props}
76
+ ref={ref}
77
+ accessibilityRole="button"
78
+ accessibilityState={{ ...accessibilityState, disabled: isDisabled, busy: isBusy }}
79
+ aria-busy={isBusy}
80
+ aria-disabled={isDisabled}
81
+ disabled={isDisabled}
82
+ className={twMerge(
83
+ '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',
84
+ containerVariants[variant],
85
+ containerSizes[size],
86
+ explicitlyDisabled && 'border-bubba-disabled bg-bubba-disabled',
87
+ className,
88
+ )}
89
+ >
90
+ {loading ? <ActivityIndicator size="small" color={spinnerColor} accessible={false} /> : null}
91
+ <Text
92
+ className={twMerge(
93
+ 'font-bold text-center',
94
+ textVariants[variant],
95
+ textSizes[size],
96
+ explicitlyDisabled && 'text-bubba-disabledText',
97
+ textClassName,
98
+ )}
99
+ >
100
+ {loading && loadingLabel ? loadingLabel : children}
101
+ </Text>
102
+ </Pressable>
103
+ );
104
+ },
105
+ );
package/src/index.ts ADDED
@@ -0,0 +1,6 @@
1
+ 'use client';
2
+
3
+ export { Button } from './components/Button';
4
+ export type { ButtonProps, ButtonSize, ButtonVariant } from './components/Button';
5
+ export { tokens } from './tokens';
6
+ export type { BubbaTokens } from './tokens';
package/src/tokens.ts ADDED
@@ -0,0 +1,23 @@
1
+ /** Pinned foundations from Bubba's Brand Studio brand release 1.0.0. */
2
+ export const tokens = {
3
+ brandVersion: '1.0.0',
4
+ colors: {
5
+ blue: '#007CE8',
6
+ red: '#CC0000',
7
+ redHover: '#A80000',
8
+ navy: '#03256C',
9
+ night: '#001845',
10
+ ink: '#131B23',
11
+ muted: '#526075',
12
+ line: '#D9E8FB',
13
+ white: '#FFFFFF',
14
+ gold: '#FED766',
15
+ disabled: '#F0F2F5',
16
+ disabledText: '#6B727E',
17
+ },
18
+ spacing: { 1: 4, 2: 8, 3: 12, 4: 16, 6: 24, 8: 32, 12: 48, 16: 64 },
19
+ radius: { control: 4, panel: 12 },
20
+ touchTarget: { minimum: 44, default: 48 },
21
+ } as const;
22
+
23
+ export type BubbaTokens = typeof tokens;
@@ -0,0 +1,14 @@
1
+ const { tokens } = require('./dist/tokens.cjs');
2
+
3
+ /** @type {import('tailwindcss').Config} */
4
+ module.exports = {
5
+ theme: {
6
+ extend: {
7
+ colors: { bubba: tokens.colors },
8
+ borderRadius: {
9
+ 'bubba-control': `${tokens.radius.control}px`,
10
+ 'bubba-panel': `${tokens.radius.panel}px`,
11
+ },
12
+ },
13
+ },
14
+ };