@bacon-pos/ui 0.15.1 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/lib/commonjs/components/SegmentedButton/SegmentedButton.js +217 -0
  2. package/lib/commonjs/components/SegmentedButton/SegmentedButton.js.map +1 -0
  3. package/lib/commonjs/components/SegmentedButton/SegmentedButton.stories.js +215 -0
  4. package/lib/commonjs/components/SegmentedButton/SegmentedButton.stories.js.map +1 -0
  5. package/lib/commonjs/components/SegmentedButton/SegmentedButton.test.js +97 -0
  6. package/lib/commonjs/components/SegmentedButton/SegmentedButton.test.js.map +1 -0
  7. package/lib/commonjs/components/SegmentedButton/index.js +13 -0
  8. package/lib/commonjs/components/SegmentedButton/index.js.map +1 -0
  9. package/lib/commonjs/components/index.js +21 -10
  10. package/lib/commonjs/components/index.js.map +1 -1
  11. package/lib/module/components/SegmentedButton/SegmentedButton.js +213 -0
  12. package/lib/module/components/SegmentedButton/SegmentedButton.js.map +1 -0
  13. package/lib/module/components/SegmentedButton/SegmentedButton.stories.js +211 -0
  14. package/lib/module/components/SegmentedButton/SegmentedButton.stories.js.map +1 -0
  15. package/lib/module/components/SegmentedButton/SegmentedButton.test.js +96 -0
  16. package/lib/module/components/SegmentedButton/SegmentedButton.test.js.map +1 -0
  17. package/lib/module/components/SegmentedButton/index.js +4 -0
  18. package/lib/module/components/SegmentedButton/index.js.map +1 -0
  19. package/lib/module/components/index.js +1 -0
  20. package/lib/module/components/index.js.map +1 -1
  21. package/lib/typescript/commonjs/components/SegmentedButton/SegmentedButton.d.ts +68 -0
  22. package/lib/typescript/commonjs/components/SegmentedButton/SegmentedButton.d.ts.map +1 -0
  23. package/lib/typescript/commonjs/components/SegmentedButton/index.d.ts +2 -0
  24. package/lib/typescript/commonjs/components/SegmentedButton/index.d.ts.map +1 -0
  25. package/lib/typescript/commonjs/components/index.d.ts +1 -0
  26. package/lib/typescript/commonjs/components/index.d.ts.map +1 -1
  27. package/lib/typescript/module/components/SegmentedButton/SegmentedButton.d.ts +68 -0
  28. package/lib/typescript/module/components/SegmentedButton/SegmentedButton.d.ts.map +1 -0
  29. package/lib/typescript/module/components/SegmentedButton/index.d.ts +2 -0
  30. package/lib/typescript/module/components/SegmentedButton/index.d.ts.map +1 -0
  31. package/lib/typescript/module/components/index.d.ts +1 -0
  32. package/lib/typescript/module/components/index.d.ts.map +1 -1
  33. package/package.json +1 -1
  34. package/src/components/SegmentedButton/SegmentedButton.tsx +269 -0
  35. package/src/components/SegmentedButton/index.ts +7 -0
  36. package/src/components/index.ts +1 -0
@@ -0,0 +1,96 @@
1
+ "use strict";
2
+
3
+ import React from 'react';
4
+ import { render, screen, fireEvent } from '@testing-library/react-native';
5
+ import { SegmentedButton } from "./SegmentedButton.js";
6
+ import { ThemeProvider } from "../../theme/index.js";
7
+ import { jsx as _jsx } from "react/jsx-runtime";
8
+ function renderWithTheme(ui) {
9
+ return render(/*#__PURE__*/_jsx(ThemeProvider, {
10
+ children: ui
11
+ }));
12
+ }
13
+ const OPTS = [{
14
+ label: 'One',
15
+ value: '1'
16
+ }, {
17
+ label: 'Two',
18
+ value: '2'
19
+ }, {
20
+ label: 'Three',
21
+ value: '3'
22
+ }];
23
+ describe('SegmentedButton', () => {
24
+ it('renders one segment per option', () => {
25
+ renderWithTheme(/*#__PURE__*/_jsx(SegmentedButton, {
26
+ options: OPTS
27
+ }));
28
+ expect(screen.getByText('One')).toBeTruthy();
29
+ expect(screen.getByText('Two')).toBeTruthy();
30
+ expect(screen.getByText('Three')).toBeTruthy();
31
+ });
32
+ it('selects the first option by default (uncontrolled) and reports changes', () => {
33
+ const onChange = jest.fn();
34
+ renderWithTheme(/*#__PURE__*/_jsx(SegmentedButton, {
35
+ options: OPTS,
36
+ onChange: onChange
37
+ }));
38
+ expect(screen.getByTestId('segment-1').props.accessibilityState.selected).toBe(true);
39
+ fireEvent.press(screen.getByText('Two'));
40
+ expect(onChange).toHaveBeenCalledWith('2');
41
+ expect(screen.getByTestId('segment-2').props.accessibilityState.selected).toBe(true);
42
+ });
43
+ it('honors defaultValue', () => {
44
+ renderWithTheme(/*#__PURE__*/_jsx(SegmentedButton, {
45
+ options: OPTS,
46
+ defaultValue: "3"
47
+ }));
48
+ expect(screen.getByTestId('segment-3').props.accessibilityState.selected).toBe(true);
49
+ });
50
+ it('respects a controlled value and does not self-update', () => {
51
+ const onChange = jest.fn();
52
+ renderWithTheme(/*#__PURE__*/_jsx(SegmentedButton, {
53
+ options: OPTS,
54
+ value: "2",
55
+ onChange: onChange
56
+ }));
57
+ expect(screen.getByTestId('segment-2').props.accessibilityState.selected).toBe(true);
58
+ fireEvent.press(screen.getByText('Three'));
59
+ expect(onChange).toHaveBeenCalledWith('3');
60
+ // value prop still "2" → stays selected
61
+ expect(screen.getByTestId('segment-2').props.accessibilityState.selected).toBe(true);
62
+ });
63
+ it('does not fire when the whole control is disabled', () => {
64
+ const onChange = jest.fn();
65
+ renderWithTheme(/*#__PURE__*/_jsx(SegmentedButton, {
66
+ options: OPTS,
67
+ disabled: true,
68
+ onChange: onChange
69
+ }));
70
+ fireEvent.press(screen.getByText('Two'));
71
+ expect(onChange).not.toHaveBeenCalled();
72
+ });
73
+ it('does not fire for a disabled segment but works for others', () => {
74
+ const onChange = jest.fn();
75
+ const opts = [{
76
+ label: 'A',
77
+ value: 'a'
78
+ }, {
79
+ label: 'B',
80
+ value: 'b',
81
+ disabled: true
82
+ }, {
83
+ label: 'C',
84
+ value: 'c'
85
+ }];
86
+ renderWithTheme(/*#__PURE__*/_jsx(SegmentedButton, {
87
+ options: opts,
88
+ onChange: onChange
89
+ }));
90
+ fireEvent.press(screen.getByText('B'));
91
+ expect(onChange).not.toHaveBeenCalled();
92
+ fireEvent.press(screen.getByText('C'));
93
+ expect(onChange).toHaveBeenCalledWith('c');
94
+ });
95
+ });
96
+ //# sourceMappingURL=SegmentedButton.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["React","render","screen","fireEvent","SegmentedButton","ThemeProvider","jsx","_jsx","renderWithTheme","ui","children","OPTS","label","value","describe","it","options","expect","getByText","toBeTruthy","onChange","jest","fn","getByTestId","props","accessibilityState","selected","toBe","press","toHaveBeenCalledWith","defaultValue","disabled","not","toHaveBeenCalled","opts"],"sourceRoot":"..\\..\\..\\..\\src","sources":["components/SegmentedButton/SegmentedButton.test.tsx"],"mappings":"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","ignoreList":[]}
@@ -0,0 +1,4 @@
1
+ "use strict";
2
+
3
+ export { SegmentedButton } from "./SegmentedButton.js";
4
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["SegmentedButton"],"sourceRoot":"..\\..\\..\\..\\src","sources":["components/SegmentedButton/index.ts"],"mappings":";;AAAA,SACEA,eAAe,QAKV,sBAAmB","ignoreList":[]}
@@ -24,6 +24,7 @@ export * from "./ProductCard/index.js";
24
24
  export * from "./ProgressBar/index.js";
25
25
  export * from "./QuantityStepper/index.js";
26
26
  export * from "./Radio/index.js";
27
+ export * from "./SegmentedButton/index.js";
27
28
  export * from "./Select/index.js";
28
29
  export * from "./Sidebar/index.js";
29
30
  export * from "./Spinner/index.js";
@@ -1 +1 @@
1
- {"version":3,"names":[],"sourceRoot":"..\\..\\..\\src","sources":["components/index.ts"],"mappings":";;AAAA,cAAc,kBAAS;AACvB,cAAc,mBAAU;AACxB,cAAc,kBAAS;AACvB,cAAc,kBAAS;AACvB,cAAc,mBAAU;AACxB,cAAc,iBAAQ;AACtB,cAAc,qBAAY;AAC1B,cAAc,iBAAQ;AACtB,cAAc,sBAAa;AAC3B,cAAc,sBAAa;AAC3B,cAAc,mBAAU;AACxB,cAAc,qBAAY;AAC1B,cAAc,uBAAc;AAC5B,cAAc,iBAAQ;AACtB,cAAc,uBAAc;AAC5B,cAAc,kBAAS;AACvB,cAAc,mBAAU;AACxB,cAAc,uBAAc;AAC5B,cAAc,iBAAQ;AACtB,cAAc,iBAAQ;AACtB,cAAc,wBAAe;AAC7B,cAAc,wBAAe;AAC7B,cAAc,4BAAmB;AACjC,cAAc,kBAAS;AACvB,cAAc,mBAAU;AACxB,cAAc,oBAAW;AACzB,cAAc,oBAAW;AACzB,cAAc,qBAAY;AAC1B,cAAc,oBAAW;AACzB,cAAc,mBAAU;AACxB,cAAc,kBAAS;AACvB,cAAc,iBAAQ;AACtB,cAAc,kBAAS;AACvB,cAAc,qBAAY","ignoreList":[]}
1
+ {"version":3,"names":[],"sourceRoot":"..\\..\\..\\src","sources":["components/index.ts"],"mappings":";;AAAA,cAAc,kBAAS;AACvB,cAAc,mBAAU;AACxB,cAAc,kBAAS;AACvB,cAAc,kBAAS;AACvB,cAAc,mBAAU;AACxB,cAAc,iBAAQ;AACtB,cAAc,qBAAY;AAC1B,cAAc,iBAAQ;AACtB,cAAc,sBAAa;AAC3B,cAAc,sBAAa;AAC3B,cAAc,mBAAU;AACxB,cAAc,qBAAY;AAC1B,cAAc,uBAAc;AAC5B,cAAc,iBAAQ;AACtB,cAAc,uBAAc;AAC5B,cAAc,kBAAS;AACvB,cAAc,mBAAU;AACxB,cAAc,uBAAc;AAC5B,cAAc,iBAAQ;AACtB,cAAc,iBAAQ;AACtB,cAAc,wBAAe;AAC7B,cAAc,wBAAe;AAC7B,cAAc,4BAAmB;AACjC,cAAc,kBAAS;AACvB,cAAc,4BAAmB;AACjC,cAAc,mBAAU;AACxB,cAAc,oBAAW;AACzB,cAAc,oBAAW;AACzB,cAAc,qBAAY;AAC1B,cAAc,oBAAW;AACzB,cAAc,mBAAU;AACxB,cAAc,kBAAS;AACvB,cAAc,iBAAQ;AACtB,cAAc,kBAAS;AACvB,cAAc,qBAAY","ignoreList":[]}
@@ -0,0 +1,68 @@
1
+ import { type StyleProp, type ViewStyle } from 'react-native';
2
+ import type { IconName } from '../Icon/icons.generated';
3
+ export type SegmentedButtonSize = 'sm' | 'md';
4
+ /** `default` = soft-rounded rectangle; `round` = fully rounded (pill). */
5
+ export type SegmentedButtonShape = 'default' | 'round';
6
+ export interface SegmentedOption {
7
+ /** Visible label. */
8
+ label: string;
9
+ /** Unique value reported on change. */
10
+ value: string;
11
+ /** Optional icon shown next to the label. */
12
+ icon?: IconName;
13
+ /** Icon side. Defaults to `left`. */
14
+ iconPosition?: 'left' | 'right';
15
+ /** Disable just this segment. */
16
+ disabled?: boolean;
17
+ }
18
+ export interface SegmentedButtonProps {
19
+ /** The segments. Two or more recommended; any count is supported. */
20
+ options: SegmentedOption[];
21
+ /** Selected value (controlled). */
22
+ value?: string;
23
+ /** Initial selected value (uncontrolled). Defaults to the first option. */
24
+ defaultValue?: string;
25
+ /** Change handler. */
26
+ onChange?: (value: string) => void;
27
+ /** Size: `sm` (16px text) or `md` (18px text, default). */
28
+ size?: SegmentedButtonSize;
29
+ /** Corner style: `default` (rounded rect) or `round` (pill). Defaults to `default`. */
30
+ shape?: SegmentedButtonShape;
31
+ /** Override the track corner radius in px (segments follow). Wins over `shape`. */
32
+ radius?: number;
33
+ /** Stretch to the container; segments share the width equally. */
34
+ fullWidth?: boolean;
35
+ /** Fixed track width in px (overrides `fullWidth`). */
36
+ width?: number;
37
+ /** Disable the whole control. */
38
+ disabled?: boolean;
39
+ /** Show the track border. Defaults to true. */
40
+ bordered?: boolean;
41
+ /** Track background color. Defaults to `background.muted`. */
42
+ trackColor?: string;
43
+ /** Selected segment background. Defaults to `action.inverse` (dark). */
44
+ selectedColor?: string;
45
+ /** Selected segment text/icon color. Defaults to `action.inverseForeground`. */
46
+ selectedTextColor?: string;
47
+ /** Unselected text/icon color. Defaults to `text.secondary`. */
48
+ textColor?: string;
49
+ /** Icon size override. Defaults to the size preset. */
50
+ iconSize?: number;
51
+ /** Slide the selected highlight between segments. Defaults to true. */
52
+ animated?: boolean;
53
+ /** Override the track container style. */
54
+ style?: StyleProp<ViewStyle>;
55
+ /** Accessibility label for the group. */
56
+ accessibilityLabel?: string;
57
+ testID?: string;
58
+ }
59
+ /**
60
+ * SegmentedButton — faithful to the Bacon POS Figma "Segmented button". A single
61
+ * connected control of 2+ options where exactly one is selected: the active
62
+ * segment fills dark with inverse text; the rest are transparent with secondary
63
+ * text, all inside a rounded track. Supports `sm`/`md` sizes, `default`/`round`
64
+ * shapes (or a custom `radius`), hug / `fullWidth` / fixed `width` layouts, icons,
65
+ * per-segment disabling, and color overrides. Controlled or uncontrolled.
66
+ */
67
+ export declare function SegmentedButton({ options, value, defaultValue, onChange, size, shape, radius, fullWidth, width, disabled, bordered, trackColor, selectedColor, selectedTextColor, textColor, iconSize, animated, style, accessibilityLabel, testID, }: SegmentedButtonProps): import("react/jsx-runtime").JSX.Element;
68
+ //# sourceMappingURL=SegmentedButton.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SegmentedButton.d.ts","sourceRoot":"","sources":["../../../../../src/components/SegmentedButton/SegmentedButton.tsx"],"names":[],"mappings":"AACA,OAAO,EAOL,KAAK,SAAS,EACd,KAAK,SAAS,EACf,MAAM,cAAc,CAAC;AAKtB,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AAGxD,MAAM,MAAM,mBAAmB,GAAG,IAAI,GAAG,IAAI,CAAC;AAC9C,0EAA0E;AAC1E,MAAM,MAAM,oBAAoB,GAAG,SAAS,GAAG,OAAO,CAAC;AAEvD,MAAM,WAAW,eAAe;IAC9B,qBAAqB;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,uCAAuC;IACvC,KAAK,EAAE,MAAM,CAAC;IACd,6CAA6C;IAC7C,IAAI,CAAC,EAAE,QAAQ,CAAC;IAChB,qCAAqC;IACrC,YAAY,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;IAChC,iCAAiC;IACjC,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,oBAAoB;IACnC,qEAAqE;IACrE,OAAO,EAAE,eAAe,EAAE,CAAC;IAC3B,mCAAmC;IACnC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,2EAA2E;IAC3E,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,sBAAsB;IACtB,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACnC,2DAA2D;IAC3D,IAAI,CAAC,EAAE,mBAAmB,CAAC;IAC3B,uFAAuF;IACvF,KAAK,CAAC,EAAE,oBAAoB,CAAC;IAC7B,mFAAmF;IACnF,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,kEAAkE;IAClE,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,uDAAuD;IACvD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,iCAAiC;IACjC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,+CAA+C;IAC/C,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,8DAA8D;IAC9D,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,wEAAwE;IACxE,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,gFAAgF;IAChF,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,gEAAgE;IAChE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,uDAAuD;IACvD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,uEAAuE;IACvE,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,0CAA0C;IAC1C,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC7B,yCAAyC;IACzC,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAkBD;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,EAC9B,OAAO,EACP,KAAK,EACL,YAAY,EACZ,QAAQ,EACR,IAAW,EACX,KAAiB,EACjB,MAAM,EACN,SAAiB,EACjB,KAAK,EACL,QAAgB,EAChB,QAAe,EACf,UAAU,EACV,aAAa,EACb,iBAAiB,EACjB,SAAS,EACT,QAAQ,EACR,QAAe,EACf,KAAK,EACL,kBAAkB,EAClB,MAAM,GACP,EAAE,oBAAoB,2CAkJtB"}
@@ -0,0 +1,2 @@
1
+ export { SegmentedButton, type SegmentedButtonProps, type SegmentedOption, type SegmentedButtonSize, type SegmentedButtonShape, } from './SegmentedButton';
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/components/SegmentedButton/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,eAAe,EACf,KAAK,oBAAoB,EACzB,KAAK,eAAe,EACpB,KAAK,mBAAmB,EACxB,KAAK,oBAAoB,GAC1B,MAAM,mBAAmB,CAAC"}
@@ -22,6 +22,7 @@ export * from './ProductCard';
22
22
  export * from './ProgressBar';
23
23
  export * from './QuantityStepper';
24
24
  export * from './Radio';
25
+ export * from './SegmentedButton';
25
26
  export * from './Select';
26
27
  export * from './Sidebar';
27
28
  export * from './Spinner';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/components/index.ts"],"names":[],"mappings":"AAAA,cAAc,SAAS,CAAC;AACxB,cAAc,UAAU,CAAC;AACzB,cAAc,SAAS,CAAC;AACxB,cAAc,SAAS,CAAC;AACxB,cAAc,UAAU,CAAC;AACzB,cAAc,QAAQ,CAAC;AACvB,cAAc,YAAY,CAAC;AAC3B,cAAc,QAAQ,CAAC;AACvB,cAAc,aAAa,CAAC;AAC5B,cAAc,aAAa,CAAC;AAC5B,cAAc,UAAU,CAAC;AACzB,cAAc,YAAY,CAAC;AAC3B,cAAc,cAAc,CAAC;AAC7B,cAAc,QAAQ,CAAC;AACvB,cAAc,cAAc,CAAC;AAC7B,cAAc,SAAS,CAAC;AACxB,cAAc,UAAU,CAAC;AACzB,cAAc,cAAc,CAAC;AAC7B,cAAc,QAAQ,CAAC;AACvB,cAAc,QAAQ,CAAC;AACvB,cAAc,eAAe,CAAC;AAC9B,cAAc,eAAe,CAAC;AAC9B,cAAc,mBAAmB,CAAC;AAClC,cAAc,SAAS,CAAC;AACxB,cAAc,UAAU,CAAC;AACzB,cAAc,WAAW,CAAC;AAC1B,cAAc,WAAW,CAAC;AAC1B,cAAc,YAAY,CAAC;AAC3B,cAAc,WAAW,CAAC;AAC1B,cAAc,UAAU,CAAC;AACzB,cAAc,SAAS,CAAC;AACxB,cAAc,QAAQ,CAAC;AACvB,cAAc,SAAS,CAAC;AACxB,cAAc,YAAY,CAAC;AAE3B,YAAY,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/components/index.ts"],"names":[],"mappings":"AAAA,cAAc,SAAS,CAAC;AACxB,cAAc,UAAU,CAAC;AACzB,cAAc,SAAS,CAAC;AACxB,cAAc,SAAS,CAAC;AACxB,cAAc,UAAU,CAAC;AACzB,cAAc,QAAQ,CAAC;AACvB,cAAc,YAAY,CAAC;AAC3B,cAAc,QAAQ,CAAC;AACvB,cAAc,aAAa,CAAC;AAC5B,cAAc,aAAa,CAAC;AAC5B,cAAc,UAAU,CAAC;AACzB,cAAc,YAAY,CAAC;AAC3B,cAAc,cAAc,CAAC;AAC7B,cAAc,QAAQ,CAAC;AACvB,cAAc,cAAc,CAAC;AAC7B,cAAc,SAAS,CAAC;AACxB,cAAc,UAAU,CAAC;AACzB,cAAc,cAAc,CAAC;AAC7B,cAAc,QAAQ,CAAC;AACvB,cAAc,QAAQ,CAAC;AACvB,cAAc,eAAe,CAAC;AAC9B,cAAc,eAAe,CAAC;AAC9B,cAAc,mBAAmB,CAAC;AAClC,cAAc,SAAS,CAAC;AACxB,cAAc,mBAAmB,CAAC;AAClC,cAAc,UAAU,CAAC;AACzB,cAAc,WAAW,CAAC;AAC1B,cAAc,WAAW,CAAC;AAC1B,cAAc,YAAY,CAAC;AAC3B,cAAc,WAAW,CAAC;AAC1B,cAAc,UAAU,CAAC;AACzB,cAAc,SAAS,CAAC;AACxB,cAAc,QAAQ,CAAC;AACvB,cAAc,SAAS,CAAC;AACxB,cAAc,YAAY,CAAC;AAE3B,YAAY,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC"}
@@ -0,0 +1,68 @@
1
+ import { type StyleProp, type ViewStyle } from 'react-native';
2
+ import type { IconName } from '../Icon/icons.generated';
3
+ export type SegmentedButtonSize = 'sm' | 'md';
4
+ /** `default` = soft-rounded rectangle; `round` = fully rounded (pill). */
5
+ export type SegmentedButtonShape = 'default' | 'round';
6
+ export interface SegmentedOption {
7
+ /** Visible label. */
8
+ label: string;
9
+ /** Unique value reported on change. */
10
+ value: string;
11
+ /** Optional icon shown next to the label. */
12
+ icon?: IconName;
13
+ /** Icon side. Defaults to `left`. */
14
+ iconPosition?: 'left' | 'right';
15
+ /** Disable just this segment. */
16
+ disabled?: boolean;
17
+ }
18
+ export interface SegmentedButtonProps {
19
+ /** The segments. Two or more recommended; any count is supported. */
20
+ options: SegmentedOption[];
21
+ /** Selected value (controlled). */
22
+ value?: string;
23
+ /** Initial selected value (uncontrolled). Defaults to the first option. */
24
+ defaultValue?: string;
25
+ /** Change handler. */
26
+ onChange?: (value: string) => void;
27
+ /** Size: `sm` (16px text) or `md` (18px text, default). */
28
+ size?: SegmentedButtonSize;
29
+ /** Corner style: `default` (rounded rect) or `round` (pill). Defaults to `default`. */
30
+ shape?: SegmentedButtonShape;
31
+ /** Override the track corner radius in px (segments follow). Wins over `shape`. */
32
+ radius?: number;
33
+ /** Stretch to the container; segments share the width equally. */
34
+ fullWidth?: boolean;
35
+ /** Fixed track width in px (overrides `fullWidth`). */
36
+ width?: number;
37
+ /** Disable the whole control. */
38
+ disabled?: boolean;
39
+ /** Show the track border. Defaults to true. */
40
+ bordered?: boolean;
41
+ /** Track background color. Defaults to `background.muted`. */
42
+ trackColor?: string;
43
+ /** Selected segment background. Defaults to `action.inverse` (dark). */
44
+ selectedColor?: string;
45
+ /** Selected segment text/icon color. Defaults to `action.inverseForeground`. */
46
+ selectedTextColor?: string;
47
+ /** Unselected text/icon color. Defaults to `text.secondary`. */
48
+ textColor?: string;
49
+ /** Icon size override. Defaults to the size preset. */
50
+ iconSize?: number;
51
+ /** Slide the selected highlight between segments. Defaults to true. */
52
+ animated?: boolean;
53
+ /** Override the track container style. */
54
+ style?: StyleProp<ViewStyle>;
55
+ /** Accessibility label for the group. */
56
+ accessibilityLabel?: string;
57
+ testID?: string;
58
+ }
59
+ /**
60
+ * SegmentedButton — faithful to the Bacon POS Figma "Segmented button". A single
61
+ * connected control of 2+ options where exactly one is selected: the active
62
+ * segment fills dark with inverse text; the rest are transparent with secondary
63
+ * text, all inside a rounded track. Supports `sm`/`md` sizes, `default`/`round`
64
+ * shapes (or a custom `radius`), hug / `fullWidth` / fixed `width` layouts, icons,
65
+ * per-segment disabling, and color overrides. Controlled or uncontrolled.
66
+ */
67
+ export declare function SegmentedButton({ options, value, defaultValue, onChange, size, shape, radius, fullWidth, width, disabled, bordered, trackColor, selectedColor, selectedTextColor, textColor, iconSize, animated, style, accessibilityLabel, testID, }: SegmentedButtonProps): import("react/jsx-runtime").JSX.Element;
68
+ //# sourceMappingURL=SegmentedButton.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SegmentedButton.d.ts","sourceRoot":"","sources":["../../../../../src/components/SegmentedButton/SegmentedButton.tsx"],"names":[],"mappings":"AACA,OAAO,EAOL,KAAK,SAAS,EACd,KAAK,SAAS,EACf,MAAM,cAAc,CAAC;AAKtB,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AAGxD,MAAM,MAAM,mBAAmB,GAAG,IAAI,GAAG,IAAI,CAAC;AAC9C,0EAA0E;AAC1E,MAAM,MAAM,oBAAoB,GAAG,SAAS,GAAG,OAAO,CAAC;AAEvD,MAAM,WAAW,eAAe;IAC9B,qBAAqB;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,uCAAuC;IACvC,KAAK,EAAE,MAAM,CAAC;IACd,6CAA6C;IAC7C,IAAI,CAAC,EAAE,QAAQ,CAAC;IAChB,qCAAqC;IACrC,YAAY,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;IAChC,iCAAiC;IACjC,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,oBAAoB;IACnC,qEAAqE;IACrE,OAAO,EAAE,eAAe,EAAE,CAAC;IAC3B,mCAAmC;IACnC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,2EAA2E;IAC3E,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,sBAAsB;IACtB,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACnC,2DAA2D;IAC3D,IAAI,CAAC,EAAE,mBAAmB,CAAC;IAC3B,uFAAuF;IACvF,KAAK,CAAC,EAAE,oBAAoB,CAAC;IAC7B,mFAAmF;IACnF,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,kEAAkE;IAClE,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,uDAAuD;IACvD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,iCAAiC;IACjC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,+CAA+C;IAC/C,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,8DAA8D;IAC9D,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,wEAAwE;IACxE,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,gFAAgF;IAChF,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,gEAAgE;IAChE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,uDAAuD;IACvD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,uEAAuE;IACvE,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,0CAA0C;IAC1C,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC7B,yCAAyC;IACzC,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAkBD;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,EAC9B,OAAO,EACP,KAAK,EACL,YAAY,EACZ,QAAQ,EACR,IAAW,EACX,KAAiB,EACjB,MAAM,EACN,SAAiB,EACjB,KAAK,EACL,QAAgB,EAChB,QAAe,EACf,UAAU,EACV,aAAa,EACb,iBAAiB,EACjB,SAAS,EACT,QAAQ,EACR,QAAe,EACf,KAAK,EACL,kBAAkB,EAClB,MAAM,GACP,EAAE,oBAAoB,2CAkJtB"}
@@ -0,0 +1,2 @@
1
+ export { SegmentedButton, type SegmentedButtonProps, type SegmentedOption, type SegmentedButtonSize, type SegmentedButtonShape, } from './SegmentedButton';
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/components/SegmentedButton/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,eAAe,EACf,KAAK,oBAAoB,EACzB,KAAK,eAAe,EACpB,KAAK,mBAAmB,EACxB,KAAK,oBAAoB,GAC1B,MAAM,mBAAmB,CAAC"}
@@ -22,6 +22,7 @@ export * from './ProductCard';
22
22
  export * from './ProgressBar';
23
23
  export * from './QuantityStepper';
24
24
  export * from './Radio';
25
+ export * from './SegmentedButton';
25
26
  export * from './Select';
26
27
  export * from './Sidebar';
27
28
  export * from './Spinner';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/components/index.ts"],"names":[],"mappings":"AAAA,cAAc,SAAS,CAAC;AACxB,cAAc,UAAU,CAAC;AACzB,cAAc,SAAS,CAAC;AACxB,cAAc,SAAS,CAAC;AACxB,cAAc,UAAU,CAAC;AACzB,cAAc,QAAQ,CAAC;AACvB,cAAc,YAAY,CAAC;AAC3B,cAAc,QAAQ,CAAC;AACvB,cAAc,aAAa,CAAC;AAC5B,cAAc,aAAa,CAAC;AAC5B,cAAc,UAAU,CAAC;AACzB,cAAc,YAAY,CAAC;AAC3B,cAAc,cAAc,CAAC;AAC7B,cAAc,QAAQ,CAAC;AACvB,cAAc,cAAc,CAAC;AAC7B,cAAc,SAAS,CAAC;AACxB,cAAc,UAAU,CAAC;AACzB,cAAc,cAAc,CAAC;AAC7B,cAAc,QAAQ,CAAC;AACvB,cAAc,QAAQ,CAAC;AACvB,cAAc,eAAe,CAAC;AAC9B,cAAc,eAAe,CAAC;AAC9B,cAAc,mBAAmB,CAAC;AAClC,cAAc,SAAS,CAAC;AACxB,cAAc,UAAU,CAAC;AACzB,cAAc,WAAW,CAAC;AAC1B,cAAc,WAAW,CAAC;AAC1B,cAAc,YAAY,CAAC;AAC3B,cAAc,WAAW,CAAC;AAC1B,cAAc,UAAU,CAAC;AACzB,cAAc,SAAS,CAAC;AACxB,cAAc,QAAQ,CAAC;AACvB,cAAc,SAAS,CAAC;AACxB,cAAc,YAAY,CAAC;AAE3B,YAAY,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/components/index.ts"],"names":[],"mappings":"AAAA,cAAc,SAAS,CAAC;AACxB,cAAc,UAAU,CAAC;AACzB,cAAc,SAAS,CAAC;AACxB,cAAc,SAAS,CAAC;AACxB,cAAc,UAAU,CAAC;AACzB,cAAc,QAAQ,CAAC;AACvB,cAAc,YAAY,CAAC;AAC3B,cAAc,QAAQ,CAAC;AACvB,cAAc,aAAa,CAAC;AAC5B,cAAc,aAAa,CAAC;AAC5B,cAAc,UAAU,CAAC;AACzB,cAAc,YAAY,CAAC;AAC3B,cAAc,cAAc,CAAC;AAC7B,cAAc,QAAQ,CAAC;AACvB,cAAc,cAAc,CAAC;AAC7B,cAAc,SAAS,CAAC;AACxB,cAAc,UAAU,CAAC;AACzB,cAAc,cAAc,CAAC;AAC7B,cAAc,QAAQ,CAAC;AACvB,cAAc,QAAQ,CAAC;AACvB,cAAc,eAAe,CAAC;AAC9B,cAAc,eAAe,CAAC;AAC9B,cAAc,mBAAmB,CAAC;AAClC,cAAc,SAAS,CAAC;AACxB,cAAc,mBAAmB,CAAC;AAClC,cAAc,UAAU,CAAC;AACzB,cAAc,WAAW,CAAC;AAC1B,cAAc,WAAW,CAAC;AAC1B,cAAc,YAAY,CAAC;AAC3B,cAAc,WAAW,CAAC;AAC1B,cAAc,UAAU,CAAC;AACzB,cAAc,SAAS,CAAC;AACxB,cAAc,QAAQ,CAAC;AACvB,cAAc,SAAS,CAAC;AACxB,cAAc,YAAY,CAAC;AAE3B,YAAY,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bacon-pos/ui",
3
- "version": "0.15.1",
3
+ "version": "0.16.0",
4
4
  "description": "Bacon POS design system — React Native component library",
5
5
  "main": "./lib/commonjs/index.js",
6
6
  "module": "./lib/module/index.js",
@@ -0,0 +1,269 @@
1
+ import { useEffect, useRef, useState } from 'react';
2
+ import {
3
+ Animated,
4
+ Easing,
5
+ Pressable,
6
+ View,
7
+ type LayoutChangeEvent,
8
+ type PressableStateCallbackType,
9
+ type StyleProp,
10
+ type ViewStyle,
11
+ } from 'react-native';
12
+ import { useTheme } from '../../theme';
13
+ import type { RadiusToken } from '../../theme/spacing';
14
+ import type { TypographyVariant } from '../../theme/typography';
15
+ import { Icon } from '../Icon/Icon';
16
+ import type { IconName } from '../Icon/icons.generated';
17
+ import { Text } from '../Text/Text';
18
+
19
+ export type SegmentedButtonSize = 'sm' | 'md';
20
+ /** `default` = soft-rounded rectangle; `round` = fully rounded (pill). */
21
+ export type SegmentedButtonShape = 'default' | 'round';
22
+
23
+ export interface SegmentedOption {
24
+ /** Visible label. */
25
+ label: string;
26
+ /** Unique value reported on change. */
27
+ value: string;
28
+ /** Optional icon shown next to the label. */
29
+ icon?: IconName;
30
+ /** Icon side. Defaults to `left`. */
31
+ iconPosition?: 'left' | 'right';
32
+ /** Disable just this segment. */
33
+ disabled?: boolean;
34
+ }
35
+
36
+ export interface SegmentedButtonProps {
37
+ /** The segments. Two or more recommended; any count is supported. */
38
+ options: SegmentedOption[];
39
+ /** Selected value (controlled). */
40
+ value?: string;
41
+ /** Initial selected value (uncontrolled). Defaults to the first option. */
42
+ defaultValue?: string;
43
+ /** Change handler. */
44
+ onChange?: (value: string) => void;
45
+ /** Size: `sm` (16px text) or `md` (18px text, default). */
46
+ size?: SegmentedButtonSize;
47
+ /** Corner style: `default` (rounded rect) or `round` (pill). Defaults to `default`. */
48
+ shape?: SegmentedButtonShape;
49
+ /** Override the track corner radius in px (segments follow). Wins over `shape`. */
50
+ radius?: number;
51
+ /** Stretch to the container; segments share the width equally. */
52
+ fullWidth?: boolean;
53
+ /** Fixed track width in px (overrides `fullWidth`). */
54
+ width?: number;
55
+ /** Disable the whole control. */
56
+ disabled?: boolean;
57
+ /** Show the track border. Defaults to true. */
58
+ bordered?: boolean;
59
+ /** Track background color. Defaults to `background.muted`. */
60
+ trackColor?: string;
61
+ /** Selected segment background. Defaults to `action.inverse` (dark). */
62
+ selectedColor?: string;
63
+ /** Selected segment text/icon color. Defaults to `action.inverseForeground`. */
64
+ selectedTextColor?: string;
65
+ /** Unselected text/icon color. Defaults to `text.secondary`. */
66
+ textColor?: string;
67
+ /** Icon size override. Defaults to the size preset. */
68
+ iconSize?: number;
69
+ /** Slide the selected highlight between segments. Defaults to true. */
70
+ animated?: boolean;
71
+ /** Override the track container style. */
72
+ style?: StyleProp<ViewStyle>;
73
+ /** Accessibility label for the group. */
74
+ accessibilityLabel?: string;
75
+ testID?: string;
76
+ }
77
+
78
+ const SIZES: Record<
79
+ SegmentedButtonSize,
80
+ {
81
+ paddingH: number;
82
+ paddingV: number;
83
+ gap: number;
84
+ trackPadding: number;
85
+ text: TypographyVariant;
86
+ icon: number;
87
+ radius: RadiusToken;
88
+ }
89
+ > = {
90
+ sm: { paddingH: 12, paddingV: 10, gap: 6, trackPadding: 4, text: 'bodyMedium', icon: 18, radius: 'md' },
91
+ md: { paddingH: 16, paddingV: 12, gap: 8, trackPadding: 5, text: 'bodyLarge', icon: 20, radius: 'lg' },
92
+ };
93
+
94
+ /**
95
+ * SegmentedButton — faithful to the Bacon POS Figma "Segmented button". A single
96
+ * connected control of 2+ options where exactly one is selected: the active
97
+ * segment fills dark with inverse text; the rest are transparent with secondary
98
+ * text, all inside a rounded track. Supports `sm`/`md` sizes, `default`/`round`
99
+ * shapes (or a custom `radius`), hug / `fullWidth` / fixed `width` layouts, icons,
100
+ * per-segment disabling, and color overrides. Controlled or uncontrolled.
101
+ */
102
+ export function SegmentedButton({
103
+ options,
104
+ value,
105
+ defaultValue,
106
+ onChange,
107
+ size = 'md',
108
+ shape = 'default',
109
+ radius,
110
+ fullWidth = false,
111
+ width,
112
+ disabled = false,
113
+ bordered = true,
114
+ trackColor,
115
+ selectedColor,
116
+ selectedTextColor,
117
+ textColor,
118
+ iconSize,
119
+ animated = true,
120
+ style,
121
+ accessibilityLabel,
122
+ testID,
123
+ }: SegmentedButtonProps) {
124
+ const theme = useTheme();
125
+ const cfg = SIZES[size];
126
+
127
+ const isControlled = value !== undefined;
128
+ const [uncontrolled, setUncontrolled] = useState(defaultValue ?? options[0]?.value ?? '');
129
+ const selected = isControlled ? value : uncontrolled;
130
+
131
+ const select = (next: string) => {
132
+ if (!isControlled) setUncontrolled(next);
133
+ onChange?.(next);
134
+ };
135
+
136
+ // Sliding highlight (thumb): we measure each segment's position/size and glide
137
+ // an absolutely-positioned fill to the selected one.
138
+ const layouts = useRef<Record<string, { x: number; width: number }>>({});
139
+ const thumbX = useRef(new Animated.Value(0)).current;
140
+ const thumbW = useRef(new Animated.Value(0)).current;
141
+ const placed = useRef(false);
142
+ const [measured, setMeasured] = useState(false);
143
+
144
+ const placeThumb = (x: number, width: number, withAnim: boolean) => {
145
+ if (withAnim && animated) {
146
+ Animated.parallel([
147
+ Animated.timing(thumbX, { toValue: x, duration: 200, easing: Easing.out(Easing.cubic), useNativeDriver: false }),
148
+ Animated.timing(thumbW, { toValue: width, duration: 200, easing: Easing.out(Easing.cubic), useNativeDriver: false }),
149
+ ]).start();
150
+ } else {
151
+ thumbX.setValue(x);
152
+ thumbW.setValue(width);
153
+ }
154
+ };
155
+
156
+ // Animate to the selected segment when it changes (once layouts are known).
157
+ useEffect(() => {
158
+ const l = layouts.current[selected];
159
+ if (!l) return;
160
+ placeThumb(l.x, l.width, placed.current);
161
+ if (!placed.current) {
162
+ placed.current = true;
163
+ setMeasured(true);
164
+ }
165
+ // eslint-disable-next-line react-hooks/exhaustive-deps
166
+ }, [selected, animated]);
167
+
168
+ const onSegmentLayout = (value: string) => (e: LayoutChangeEvent) => {
169
+ const { x, width } = e.nativeEvent.layout;
170
+ layouts.current[value] = { x, width };
171
+ // Place/keep the thumb aligned with the selected segment (initial mount and
172
+ // on relayout/resize) without animating.
173
+ if (value === selected) {
174
+ placeThumb(x, width, false);
175
+ if (!placed.current) {
176
+ placed.current = true;
177
+ setMeasured(true);
178
+ }
179
+ }
180
+ };
181
+
182
+ const trackRadius = radius ?? (shape === 'round' ? theme.radii.pill : theme.radii.xl);
183
+ const segmentRadius =
184
+ shape === 'round'
185
+ ? theme.radii.pill
186
+ : radius != null
187
+ ? Math.max(radius - cfg.trackPadding, 0)
188
+ : theme.radii[cfg.radius];
189
+
190
+ const selectedBg = selectedColor ?? theme.colors.action.inverse;
191
+ const selectedFg = selectedTextColor ?? theme.colors.action.inverseForeground;
192
+ const restFg = textColor ?? theme.colors.text.secondary;
193
+ const resolvedIconSize = iconSize ?? cfg.icon;
194
+
195
+ return (
196
+ <View
197
+ testID={testID}
198
+ accessibilityRole="radiogroup"
199
+ accessibilityLabel={accessibilityLabel}
200
+ style={[
201
+ {
202
+ padding: cfg.trackPadding,
203
+ borderRadius: trackRadius,
204
+ borderWidth: bordered ? 1 : 0,
205
+ borderColor: theme.colors.border.default,
206
+ backgroundColor: trackColor ?? theme.colors.background.muted,
207
+ alignSelf: fullWidth && width == null ? 'stretch' : 'flex-start',
208
+ width: width ?? (fullWidth ? '100%' : undefined),
209
+ opacity: disabled ? 0.5 : 1,
210
+ },
211
+ style,
212
+ ]}
213
+ >
214
+ {/* Inner row: shares an unpadded coordinate space with the thumb so the
215
+ measured segment x/width line up exactly. */}
216
+ <View style={{ flexDirection: 'row', alignItems: 'stretch', gap: cfg.gap, position: 'relative' }}>
217
+ <Animated.View
218
+ pointerEvents="none"
219
+ style={{
220
+ position: 'absolute',
221
+ top: 0,
222
+ bottom: 0,
223
+ left: thumbX,
224
+ width: thumbW,
225
+ borderRadius: segmentRadius,
226
+ backgroundColor: selectedBg,
227
+ opacity: measured ? 1 : 0,
228
+ }}
229
+ />
230
+ {options.map((opt) => {
231
+ const isSelected = opt.value === selected;
232
+ const isDisabled = disabled || opt.disabled;
233
+ const fg = isSelected ? selectedFg : restFg;
234
+ const iconNode = opt.icon ? <Icon name={opt.icon} size={resolvedIconSize} color={fg} /> : null;
235
+ const iconRight = opt.iconPosition === 'right';
236
+ return (
237
+ <Pressable
238
+ key={opt.value}
239
+ onLayout={onSegmentLayout(opt.value)}
240
+ onPress={isDisabled ? undefined : () => select(opt.value)}
241
+ disabled={isDisabled}
242
+ accessibilityRole="button"
243
+ accessibilityState={{ selected: isSelected, disabled: isDisabled }}
244
+ accessibilityLabel={opt.label}
245
+ testID={`segment-${opt.value}`}
246
+ style={({ pressed }: PressableStateCallbackType): StyleProp<ViewStyle> => ({
247
+ flex: fullWidth || width != null ? 1 : undefined,
248
+ flexDirection: 'row',
249
+ alignItems: 'center',
250
+ justifyContent: 'center',
251
+ gap: cfg.gap,
252
+ paddingHorizontal: cfg.paddingH,
253
+ paddingVertical: cfg.paddingV,
254
+ borderRadius: segmentRadius,
255
+ opacity: opt.disabled ? 0.5 : pressed && !isSelected ? 0.6 : 1,
256
+ })}
257
+ >
258
+ {iconNode && !iconRight ? iconNode : null}
259
+ <Text variant={cfg.text} weight="medium" color={fg} numberOfLines={1}>
260
+ {opt.label}
261
+ </Text>
262
+ {iconNode && iconRight ? iconNode : null}
263
+ </Pressable>
264
+ );
265
+ })}
266
+ </View>
267
+ </View>
268
+ );
269
+ }
@@ -0,0 +1,7 @@
1
+ export {
2
+ SegmentedButton,
3
+ type SegmentedButtonProps,
4
+ type SegmentedOption,
5
+ type SegmentedButtonSize,
6
+ type SegmentedButtonShape,
7
+ } from './SegmentedButton';
@@ -22,6 +22,7 @@ export * from './ProductCard';
22
22
  export * from './ProgressBar';
23
23
  export * from './QuantityStepper';
24
24
  export * from './Radio';
25
+ export * from './SegmentedButton';
25
26
  export * from './Select';
26
27
  export * from './Sidebar';
27
28
  export * from './Spinner';