@oxyhq/bloom 0.5.0 → 0.6.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 (142) hide show
  1. package/lib/commonjs/error-boundary/ErrorBoundary.js +27 -7
  2. package/lib/commonjs/error-boundary/ErrorBoundary.js.map +1 -1
  3. package/lib/commonjs/fonts/FontLoader.js +6 -5
  4. package/lib/commonjs/fonts/FontLoader.js.map +1 -1
  5. package/lib/commonjs/fonts/apply-font-faces.js +4 -4
  6. package/lib/commonjs/fonts/apply-font-faces.web.js +13 -12
  7. package/lib/commonjs/fonts/apply-font-faces.web.js.map +1 -1
  8. package/lib/commonjs/fonts/font-assets.js +2 -2
  9. package/lib/commonjs/fonts/font-data.web.js +22 -0
  10. package/lib/commonjs/fonts/font-data.web.js.map +1 -0
  11. package/lib/commonjs/index.js.map +1 -1
  12. package/lib/commonjs/index.web.js.map +1 -1
  13. package/lib/commonjs/skeleton/index.js +30 -0
  14. package/lib/commonjs/skeleton/index.js.map +1 -1
  15. package/lib/module/error-boundary/ErrorBoundary.js +27 -7
  16. package/lib/module/error-boundary/ErrorBoundary.js.map +1 -1
  17. package/lib/module/fonts/FontLoader.js +6 -5
  18. package/lib/module/fonts/FontLoader.js.map +1 -1
  19. package/lib/module/fonts/apply-font-faces.js +4 -4
  20. package/lib/module/fonts/apply-font-faces.web.js +13 -10
  21. package/lib/module/fonts/apply-font-faces.web.js.map +1 -1
  22. package/lib/module/fonts/font-assets.js +2 -2
  23. package/lib/module/fonts/font-data.web.js +18 -0
  24. package/lib/module/fonts/font-data.web.js.map +1 -0
  25. package/lib/module/fonts/index.web.js +4 -4
  26. package/lib/module/index.js.map +1 -1
  27. package/lib/module/index.web.js.map +1 -1
  28. package/lib/module/skeleton/index.js +29 -0
  29. package/lib/module/skeleton/index.js.map +1 -1
  30. package/lib/typescript/commonjs/error-boundary/ErrorBoundary.d.ts +3 -1
  31. package/lib/typescript/commonjs/error-boundary/ErrorBoundary.d.ts.map +1 -1
  32. package/lib/typescript/commonjs/error-boundary/index.d.ts +1 -1
  33. package/lib/typescript/commonjs/error-boundary/index.d.ts.map +1 -1
  34. package/lib/typescript/commonjs/error-boundary/types.d.ts +41 -2
  35. package/lib/typescript/commonjs/error-boundary/types.d.ts.map +1 -1
  36. package/lib/typescript/commonjs/fonts/FontLoader.d.ts.map +1 -1
  37. package/lib/typescript/commonjs/fonts/apply-font-faces.web.d.ts +8 -1
  38. package/lib/typescript/commonjs/fonts/apply-font-faces.web.d.ts.map +1 -1
  39. package/lib/typescript/commonjs/fonts/font-data.web.d.ts +5 -0
  40. package/lib/typescript/commonjs/fonts/font-data.web.d.ts.map +1 -0
  41. package/lib/typescript/commonjs/index.d.ts +1 -1
  42. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  43. package/lib/typescript/commonjs/index.web.d.ts +1 -1
  44. package/lib/typescript/commonjs/index.web.d.ts.map +1 -1
  45. package/lib/typescript/commonjs/skeleton/index.d.ts +24 -1
  46. package/lib/typescript/commonjs/skeleton/index.d.ts.map +1 -1
  47. package/lib/typescript/module/error-boundary/ErrorBoundary.d.ts +3 -1
  48. package/lib/typescript/module/error-boundary/ErrorBoundary.d.ts.map +1 -1
  49. package/lib/typescript/module/error-boundary/index.d.ts +1 -1
  50. package/lib/typescript/module/error-boundary/index.d.ts.map +1 -1
  51. package/lib/typescript/module/error-boundary/types.d.ts +41 -2
  52. package/lib/typescript/module/error-boundary/types.d.ts.map +1 -1
  53. package/lib/typescript/module/fonts/FontLoader.d.ts.map +1 -1
  54. package/lib/typescript/module/fonts/apply-font-faces.web.d.ts +8 -1
  55. package/lib/typescript/module/fonts/apply-font-faces.web.d.ts.map +1 -1
  56. package/lib/typescript/module/fonts/font-data.web.d.ts +5 -0
  57. package/lib/typescript/module/fonts/font-data.web.d.ts.map +1 -0
  58. package/lib/typescript/module/index.d.ts +1 -1
  59. package/lib/typescript/module/index.d.ts.map +1 -1
  60. package/lib/typescript/module/index.web.d.ts +1 -1
  61. package/lib/typescript/module/index.web.d.ts.map +1 -1
  62. package/lib/typescript/module/skeleton/index.d.ts +24 -1
  63. package/lib/typescript/module/skeleton/index.d.ts.map +1 -1
  64. package/package.json +36 -5
  65. package/src/__tests__/ErrorBoundary.test.tsx +217 -0
  66. package/src/__tests__/Skeleton.test.tsx +63 -0
  67. package/src/avatar/Avatar.stories.tsx +69 -0
  68. package/src/bottom-sheet/BottomSheet.stories.tsx +92 -0
  69. package/src/button/Button.stories.tsx +94 -0
  70. package/src/context-menu/ContextMenu.stories.tsx +71 -0
  71. package/src/dialog/Dialog.stories.tsx +112 -0
  72. package/src/error-boundary/ErrorBoundary.tsx +28 -5
  73. package/src/error-boundary/index.ts +5 -1
  74. package/src/error-boundary/types.ts +45 -2
  75. package/src/fonts/FontLoader.tsx +6 -5
  76. package/src/fonts/apply-font-faces.ts +4 -4
  77. package/src/fonts/apply-font-faces.web.ts +18 -10
  78. package/src/fonts/font-assets.ts +2 -2
  79. package/src/fonts/font-data.web.ts +15 -0
  80. package/src/fonts/index.web.ts +4 -4
  81. package/src/index.ts +5 -1
  82. package/src/index.web.ts +5 -1
  83. package/src/loading/Loading.stories.tsx +60 -0
  84. package/src/menu/Menu.stories.tsx +79 -0
  85. package/src/prompt-input/PromptInput.stories.tsx +82 -0
  86. package/src/select/Select.stories.tsx +84 -0
  87. package/src/settings-list/SettingsList.stories.tsx +106 -0
  88. package/src/skeleton/index.tsx +54 -1
  89. package/src/text-field/TextField.stories.tsx +90 -0
  90. package/src/toast/Toast.stories.tsx +109 -0
  91. package/lib/commonjs/fonts/assets/BlomusModernus-Bold.woff2 +0 -0
  92. package/lib/commonjs/fonts/assets/BlomusModernus-Regular.woff2 +0 -0
  93. package/lib/commonjs/fonts/assets/GeistMono-Variable.woff2 +0 -0
  94. package/lib/commonjs/fonts/assets/InterVariable.woff2 +0 -0
  95. package/lib/module/fonts/assets/BlomusModernus-Bold.woff2 +0 -0
  96. package/lib/module/fonts/assets/BlomusModernus-Regular.woff2 +0 -0
  97. package/lib/module/fonts/assets/GeistMono-Variable.woff2 +0 -0
  98. package/lib/module/fonts/assets/InterVariable.woff2 +0 -0
  99. package/lib/typescript/commonjs/__tests__/BloomThemeProvider.fonts-web.test.d.ts +0 -5
  100. package/lib/typescript/commonjs/__tests__/BloomThemeProvider.fonts-web.test.d.ts.map +0 -1
  101. package/lib/typescript/commonjs/__tests__/BloomThemeProvider.test.d.ts +0 -2
  102. package/lib/typescript/commonjs/__tests__/BloomThemeProvider.test.d.ts.map +0 -1
  103. package/lib/typescript/commonjs/__tests__/BottomSheet.test.d.ts +0 -2
  104. package/lib/typescript/commonjs/__tests__/BottomSheet.test.d.ts.map +0 -1
  105. package/lib/typescript/commonjs/__tests__/Button.test.d.ts +0 -2
  106. package/lib/typescript/commonjs/__tests__/Button.test.d.ts.map +0 -1
  107. package/lib/typescript/commonjs/__tests__/Code.test.d.ts +0 -2
  108. package/lib/typescript/commonjs/__tests__/Code.test.d.ts.map +0 -1
  109. package/lib/typescript/commonjs/__tests__/Dialog.test.d.ts +0 -2
  110. package/lib/typescript/commonjs/__tests__/Dialog.test.d.ts.map +0 -1
  111. package/lib/typescript/commonjs/__tests__/FontLoader.native.test.d.ts +0 -2
  112. package/lib/typescript/commonjs/__tests__/FontLoader.native.test.d.ts.map +0 -1
  113. package/lib/typescript/commonjs/__tests__/Pre.test.d.ts +0 -2
  114. package/lib/typescript/commonjs/__tests__/Pre.test.d.ts.map +0 -1
  115. package/lib/typescript/commonjs/__tests__/SettingsList.test.d.ts +0 -2
  116. package/lib/typescript/commonjs/__tests__/SettingsList.test.d.ts.map +0 -1
  117. package/lib/typescript/commonjs/__tests__/apply-font-faces.test.d.ts +0 -5
  118. package/lib/typescript/commonjs/__tests__/apply-font-faces.test.d.ts.map +0 -1
  119. package/lib/typescript/commonjs/__tests__/theme.test.d.ts +0 -2
  120. package/lib/typescript/commonjs/__tests__/theme.test.d.ts.map +0 -1
  121. package/lib/typescript/module/__tests__/BloomThemeProvider.fonts-web.test.d.ts +0 -5
  122. package/lib/typescript/module/__tests__/BloomThemeProvider.fonts-web.test.d.ts.map +0 -1
  123. package/lib/typescript/module/__tests__/BloomThemeProvider.test.d.ts +0 -2
  124. package/lib/typescript/module/__tests__/BloomThemeProvider.test.d.ts.map +0 -1
  125. package/lib/typescript/module/__tests__/BottomSheet.test.d.ts +0 -2
  126. package/lib/typescript/module/__tests__/BottomSheet.test.d.ts.map +0 -1
  127. package/lib/typescript/module/__tests__/Button.test.d.ts +0 -2
  128. package/lib/typescript/module/__tests__/Button.test.d.ts.map +0 -1
  129. package/lib/typescript/module/__tests__/Code.test.d.ts +0 -2
  130. package/lib/typescript/module/__tests__/Code.test.d.ts.map +0 -1
  131. package/lib/typescript/module/__tests__/Dialog.test.d.ts +0 -2
  132. package/lib/typescript/module/__tests__/Dialog.test.d.ts.map +0 -1
  133. package/lib/typescript/module/__tests__/FontLoader.native.test.d.ts +0 -2
  134. package/lib/typescript/module/__tests__/FontLoader.native.test.d.ts.map +0 -1
  135. package/lib/typescript/module/__tests__/Pre.test.d.ts +0 -2
  136. package/lib/typescript/module/__tests__/Pre.test.d.ts.map +0 -1
  137. package/lib/typescript/module/__tests__/SettingsList.test.d.ts +0 -2
  138. package/lib/typescript/module/__tests__/SettingsList.test.d.ts.map +0 -1
  139. package/lib/typescript/module/__tests__/apply-font-faces.test.d.ts +0 -5
  140. package/lib/typescript/module/__tests__/apply-font-faces.test.d.ts.map +0 -1
  141. package/lib/typescript/module/__tests__/theme.test.d.ts +0 -2
  142. package/lib/typescript/module/__tests__/theme.test.d.ts.map +0 -1
@@ -1,10 +1,10 @@
1
1
  // Web variant of the `./fonts` barrel.
2
2
  //
3
3
  // The default barrel (`./index.ts`) re-exports `applyFontFaces` from
4
- // `./apply-font-faces`, which on native resolves to a no-op stub Metro
5
- // cannot parse `.woff2` module-level imports. The web fork explicitly
6
- // reaches for `./apply-font-faces.web`, which performs the real `@font-face`
7
- // injection.
4
+ // `./apply-font-faces`, which on native is a no-op stub. The web fork
5
+ // explicitly reaches for `./apply-font-faces.web`, which performs the real
6
+ // `@font-face` injection using inlined base64 data URLs (see
7
+ // `font-data.web.ts`).
8
8
  //
9
9
  // Web bundlers select this file via the `"browser"` condition in
10
10
  // `package.json`'s `exports['./fonts']`; native bundlers fall through to
package/src/index.ts CHANGED
@@ -40,7 +40,11 @@ export * from './divider';
40
40
  export * from './radio-indicator';
41
41
  export * from './collapsible';
42
42
  export { ErrorBoundary } from './error-boundary';
43
- export type { ErrorBoundaryProps } from './error-boundary';
43
+ export type {
44
+ ErrorBoundaryProps,
45
+ ErrorBoundaryFallback,
46
+ ErrorBoundaryFallbackContext,
47
+ } from './error-boundary';
44
48
  export * from './avatar';
45
49
  export * from './loading';
46
50
  export * as PromptInput from './prompt-input';
package/src/index.web.ts CHANGED
@@ -45,7 +45,11 @@ export * from './divider';
45
45
  export * from './radio-indicator';
46
46
  export * from './collapsible';
47
47
  export { ErrorBoundary } from './error-boundary';
48
- export type { ErrorBoundaryProps } from './error-boundary';
48
+ export type {
49
+ ErrorBoundaryProps,
50
+ ErrorBoundaryFallback,
51
+ ErrorBoundaryFallbackContext,
52
+ } from './error-boundary';
49
53
  export * from './avatar';
50
54
  export * from './loading';
51
55
  export * as PromptInput from './prompt-input';
@@ -0,0 +1,60 @@
1
+ import React from 'react';
2
+ import { View } from 'react-native';
3
+ import type { Meta, StoryObj } from '@storybook/react-vite';
4
+
5
+ import { Loading } from './Loading';
6
+
7
+ const meta: Meta<typeof Loading> = {
8
+ title: 'Components/Loading',
9
+ component: Loading,
10
+ argTypes: {
11
+ variant: {
12
+ control: 'select',
13
+ options: ['spinner', 'top', 'skeleton', 'inline'],
14
+ },
15
+ size: {
16
+ control: 'select',
17
+ options: ['small', 'medium', 'large'],
18
+ },
19
+ },
20
+ };
21
+
22
+ export default meta;
23
+
24
+ type Story = StoryObj<typeof Loading>;
25
+
26
+ export const Basic: Story = {
27
+ args: { variant: 'spinner' },
28
+ };
29
+
30
+ export const Spinner: Story = {
31
+ args: { variant: 'spinner', text: 'Loading…', showText: true },
32
+ };
33
+
34
+ export const Inline: Story = {
35
+ args: { variant: 'inline', text: 'Saving' },
36
+ };
37
+
38
+ export const Skeleton: Story = {
39
+ args: { variant: 'skeleton', lines: 4 },
40
+ };
41
+
42
+ export const Sizes: Story = {
43
+ render: () => (
44
+ <View style={{ flexDirection: 'row', gap: 24, alignItems: 'center' }}>
45
+ <Loading variant="spinner" size="small" />
46
+ <Loading variant="spinner" size="medium" />
47
+ <Loading variant="spinner" size="large" />
48
+ </View>
49
+ ),
50
+ };
51
+
52
+ export const Composition: Story = {
53
+ render: () => (
54
+ <View style={{ gap: 24, alignItems: 'flex-start' }}>
55
+ <Loading variant="spinner" size="small" text="Small" />
56
+ <Loading variant="inline" text="Saving changes" />
57
+ <Loading variant="skeleton" lines={3} />
58
+ </View>
59
+ ),
60
+ };
@@ -0,0 +1,79 @@
1
+ import React from 'react';
2
+ import { View } from 'react-native';
3
+ import type { Meta, StoryObj } from '@storybook/react-vite';
4
+
5
+ import { Button } from '../button';
6
+ import * as Menu from './index';
7
+
8
+ const meta: Meta = {
9
+ title: 'Components/Menu',
10
+ };
11
+
12
+ export default meta;
13
+
14
+ type Story = StoryObj;
15
+
16
+ function BasicMenu() {
17
+ return (
18
+ <Menu.Root>
19
+ <Menu.Trigger label="Open menu">
20
+ {({ props }) => (
21
+ <Button onPress={props.onPress}>Open menu</Button>
22
+ )}
23
+ </Menu.Trigger>
24
+ <Menu.Outer>
25
+ <Menu.Group>
26
+ <Menu.Item label="Profile" onPress={() => {}}>
27
+ <Menu.ItemText>Profile</Menu.ItemText>
28
+ </Menu.Item>
29
+ <Menu.Item label="Settings" onPress={() => {}}>
30
+ <Menu.ItemText>Settings</Menu.ItemText>
31
+ </Menu.Item>
32
+ <Menu.Item label="Sign out" onPress={() => {}}>
33
+ <Menu.ItemText>Sign out</Menu.ItemText>
34
+ </Menu.Item>
35
+ </Menu.Group>
36
+ </Menu.Outer>
37
+ </Menu.Root>
38
+ );
39
+ }
40
+
41
+ function MenuWithDisabledItem() {
42
+ return (
43
+ <Menu.Root>
44
+ <Menu.Trigger label="Open menu with disabled item">
45
+ {({ props }) => <Button onPress={props.onPress}>Open</Button>}
46
+ </Menu.Trigger>
47
+ <Menu.Outer>
48
+ <Menu.Group>
49
+ <Menu.Item label="Edit" onPress={() => {}}>
50
+ <Menu.ItemText>Edit</Menu.ItemText>
51
+ </Menu.Item>
52
+ <Menu.Item label="Duplicate" onPress={() => {}} disabled>
53
+ <Menu.ItemText>Duplicate (disabled)</Menu.ItemText>
54
+ </Menu.Item>
55
+ <Menu.Item label="Delete" onPress={() => {}}>
56
+ <Menu.ItemText>Delete</Menu.ItemText>
57
+ </Menu.Item>
58
+ </Menu.Group>
59
+ </Menu.Outer>
60
+ </Menu.Root>
61
+ );
62
+ }
63
+
64
+ export const Basic: Story = {
65
+ render: () => <BasicMenu />,
66
+ };
67
+
68
+ export const WithDisabled: Story = {
69
+ render: () => <MenuWithDisabledItem />,
70
+ };
71
+
72
+ export const Composition: Story = {
73
+ render: () => (
74
+ <View style={{ gap: 12, alignItems: 'flex-start' }}>
75
+ <BasicMenu />
76
+ <MenuWithDisabledItem />
77
+ </View>
78
+ ),
79
+ };
@@ -0,0 +1,82 @@
1
+ import React, { useState } from 'react';
2
+ import { View } from 'react-native';
3
+ import type { Meta, StoryObj } from '@storybook/react-vite';
4
+
5
+ import { PromptInput } from './PromptInput';
6
+
7
+ const meta: Meta<typeof PromptInput> = {
8
+ title: 'Components/PromptInput',
9
+ component: PromptInput,
10
+ };
11
+
12
+ export default meta;
13
+
14
+ type Story = StoryObj<typeof PromptInput>;
15
+
16
+ function BasicPrompt() {
17
+ const [value, setValue] = useState('');
18
+ return (
19
+ <View style={{ width: 480 }}>
20
+ <PromptInput
21
+ value={value}
22
+ onValueChange={setValue}
23
+ placeholder="Ask anything"
24
+ onSubmit={() => {
25
+ // submitted
26
+ setValue('');
27
+ }}
28
+ />
29
+ </View>
30
+ );
31
+ }
32
+
33
+ function LoadingPrompt() {
34
+ const [value, setValue] = useState('Generating response...');
35
+ return (
36
+ <View style={{ width: 480 }}>
37
+ <PromptInput
38
+ value={value}
39
+ onValueChange={setValue}
40
+ placeholder="Ask anything"
41
+ isLoading
42
+ onSubmit={() => {}}
43
+ onStop={() => {}}
44
+ />
45
+ </View>
46
+ );
47
+ }
48
+
49
+ function DisabledPrompt() {
50
+ return (
51
+ <View style={{ width: 480 }}>
52
+ <PromptInput
53
+ value=""
54
+ placeholder="Disabled"
55
+ disabled
56
+ onSubmit={() => {}}
57
+ />
58
+ </View>
59
+ );
60
+ }
61
+
62
+ export const Basic: Story = {
63
+ render: () => <BasicPrompt />,
64
+ };
65
+
66
+ export const Loading: Story = {
67
+ render: () => <LoadingPrompt />,
68
+ };
69
+
70
+ export const Disabled: Story = {
71
+ render: () => <DisabledPrompt />,
72
+ };
73
+
74
+ export const Composition: Story = {
75
+ render: () => (
76
+ <View style={{ gap: 16 }}>
77
+ <BasicPrompt />
78
+ <LoadingPrompt />
79
+ <DisabledPrompt />
80
+ </View>
81
+ ),
82
+ };
@@ -0,0 +1,84 @@
1
+ import React, { useState } from 'react';
2
+ import { View } from 'react-native';
3
+ import type { Meta, StoryObj } from '@storybook/react-vite';
4
+
5
+ import * as Select from './index';
6
+
7
+ const meta: Meta = {
8
+ title: 'Components/Select',
9
+ };
10
+
11
+ export default meta;
12
+
13
+ type Story = StoryObj;
14
+
15
+ type Option = { value: string; label: string };
16
+
17
+ const FRUITS: Option[] = [
18
+ { value: 'apple', label: 'Apple' },
19
+ { value: 'banana', label: 'Banana' },
20
+ { value: 'cherry', label: 'Cherry' },
21
+ { value: 'durian', label: 'Durian' },
22
+ { value: 'elderberry', label: 'Elderberry' },
23
+ ];
24
+
25
+ function BasicSelect() {
26
+ const [value, setValue] = useState<string>('apple');
27
+ return (
28
+ <Select.Root value={value} onValueChange={setValue}>
29
+ <Select.Trigger label="Pick a fruit">
30
+ <Select.ValueText placeholder="Pick a fruit" />
31
+ <Select.Icon />
32
+ </Select.Trigger>
33
+ <Select.Content
34
+ label="Pick a fruit"
35
+ items={FRUITS}
36
+ renderItem={(item) => (
37
+ <Select.Item value={item.value} label={item.label}>
38
+ <Select.ItemIndicator />
39
+ <Select.ItemText>{item.label}</Select.ItemText>
40
+ </Select.Item>
41
+ )}
42
+ />
43
+ </Select.Root>
44
+ );
45
+ }
46
+
47
+ function UncontrolledSelect() {
48
+ const [value, setValue] = useState<string | undefined>(undefined);
49
+ return (
50
+ <Select.Root value={value} onValueChange={setValue}>
51
+ <Select.Trigger label="Pick a fruit">
52
+ <Select.ValueText placeholder="No fruit selected" />
53
+ <Select.Icon />
54
+ </Select.Trigger>
55
+ <Select.Content
56
+ label="Pick a fruit"
57
+ items={FRUITS}
58
+ renderItem={(item) => (
59
+ <Select.Item value={item.value} label={item.label}>
60
+ <Select.ItemIndicator />
61
+ <Select.ItemText>{item.label}</Select.ItemText>
62
+ </Select.Item>
63
+ )}
64
+ />
65
+ </Select.Root>
66
+ );
67
+ }
68
+
69
+ export const Basic: Story = {
70
+ render: () => <BasicSelect />,
71
+ };
72
+
73
+ export const WithPlaceholder: Story = {
74
+ render: () => <UncontrolledSelect />,
75
+ };
76
+
77
+ export const Composition: Story = {
78
+ render: () => (
79
+ <View style={{ gap: 16 }}>
80
+ <BasicSelect />
81
+ <UncontrolledSelect />
82
+ </View>
83
+ ),
84
+ };
@@ -0,0 +1,106 @@
1
+ import React, { useState } from 'react';
2
+ import { View } from 'react-native';
3
+ import type { Meta, StoryObj } from '@storybook/react-vite';
4
+
5
+ import { Switch } from '../switch';
6
+ import {
7
+ SettingsListGroup,
8
+ SettingsListItem,
9
+ SettingsListDivider,
10
+ } from './SettingsList';
11
+
12
+ const meta: Meta = {
13
+ title: 'Components/SettingsList',
14
+ };
15
+
16
+ export default meta;
17
+
18
+ type Story = StoryObj;
19
+
20
+ function BasicList() {
21
+ return (
22
+ <View style={{ width: 360 }}>
23
+ <SettingsListGroup title="Account">
24
+ <SettingsListItem
25
+ title="Profile"
26
+ description="Manage your account profile"
27
+ onPress={() => {}}
28
+ />
29
+ <SettingsListItem
30
+ title="Email"
31
+ value="nate@oxy.so"
32
+ onPress={() => {}}
33
+ />
34
+ <SettingsListItem
35
+ title="Sign out"
36
+ destructive
37
+ onPress={() => {}}
38
+ />
39
+ </SettingsListGroup>
40
+ </View>
41
+ );
42
+ }
43
+
44
+ function WithToggles() {
45
+ const [notifs, setNotifs] = useState(true);
46
+ const [dark, setDark] = useState(false);
47
+ return (
48
+ <View style={{ width: 360 }}>
49
+ <SettingsListGroup title="Preferences" footer="Changes apply immediately.">
50
+ <SettingsListItem
51
+ title="Push notifications"
52
+ rightElement={
53
+ <Switch value={notifs} onValueChange={setNotifs} />
54
+ }
55
+ showChevron={false}
56
+ />
57
+ <SettingsListItem
58
+ title="Dark mode"
59
+ rightElement={<Switch value={dark} onValueChange={setDark} />}
60
+ showChevron={false}
61
+ />
62
+ </SettingsListGroup>
63
+ </View>
64
+ );
65
+ }
66
+
67
+ function DescriptionList() {
68
+ return (
69
+ <View style={{ width: 360 }}>
70
+ <SettingsListGroup title="Security">
71
+ <SettingsListItem
72
+ title="Two-factor authentication"
73
+ description="Add an extra layer of security to your account."
74
+ onPress={() => {}}
75
+ />
76
+ <SettingsListItem
77
+ title="Active sessions"
78
+ description="View where your account is signed in."
79
+ onPress={() => {}}
80
+ />
81
+ </SettingsListGroup>
82
+ </View>
83
+ );
84
+ }
85
+
86
+ export const Basic: Story = {
87
+ render: () => <BasicList />,
88
+ };
89
+
90
+ export const WithRightElements: Story = {
91
+ render: () => <WithToggles />,
92
+ };
93
+
94
+ export const WithDescriptions: Story = {
95
+ render: () => <DescriptionList />,
96
+ };
97
+
98
+ export const Composition: Story = {
99
+ render: () => (
100
+ <View style={{ gap: 24, width: 360 }}>
101
+ <DescriptionList />
102
+ <SettingsListDivider />
103
+ <WithToggles />
104
+ </View>
105
+ ),
106
+ };
@@ -1,5 +1,12 @@
1
1
  import React, { useEffect, useRef } from 'react';
2
- import { Animated, View, type ViewStyle, type TextStyle, StyleSheet } from 'react-native';
2
+ import {
3
+ Animated,
4
+ View,
5
+ type ViewStyle,
6
+ type TextStyle,
7
+ type DimensionValue,
8
+ StyleSheet,
9
+ } from 'react-native';
3
10
 
4
11
  import { useTheme } from '../theme/use-theme';
5
12
  import { SUPPORTS_NATIVE_DRIVER } from '../styles/native-driver';
@@ -129,6 +136,52 @@ export function Pill({
129
136
  }
130
137
  Pill.displayName = 'Skeleton.Pill';
131
138
 
139
+ /**
140
+ * Generic rectangular shimmering placeholder. Use for image/card/banner
141
+ * placeholders where neither {@link Pill} (forced borderRadius: 999) nor
142
+ * {@link Circle} fit. Defaults to a small borderRadius (8). Pass `0` for
143
+ * sharp corners or any larger number / percentage string for custom shapes.
144
+ */
145
+ export function Box({
146
+ width,
147
+ height,
148
+ borderRadius = 8,
149
+ blend,
150
+ style,
151
+ }: {
152
+ /** Width as a number, percentage string, or any valid RN dimension. */
153
+ width?: DimensionValue;
154
+ /** Height as a number, percentage string, or any valid RN dimension. */
155
+ height?: DimensionValue;
156
+ /**
157
+ * Corner radius — number or percentage string. Defaults to 8. Pass 0 for
158
+ * sharp corners.
159
+ */
160
+ borderRadius?: ViewStyle['borderRadius'];
161
+ /** When true, dampens shimmer opacity (use for nested skeletons). */
162
+ blend?: boolean;
163
+ style?: ViewStyle | ViewStyle[];
164
+ }) {
165
+ const { colors } = useTheme();
166
+ const shimmer = useShimmer();
167
+
168
+ return (
169
+ <Animated.View
170
+ style={[
171
+ {
172
+ backgroundColor: colors.contrast50,
173
+ width,
174
+ height,
175
+ borderRadius,
176
+ opacity: Animated.multiply(shimmer, blend ? 0.6 : 1),
177
+ },
178
+ style,
179
+ ]}
180
+ />
181
+ );
182
+ }
183
+ Box.displayName = 'Skeleton.Box';
184
+
132
185
  export function Col({
133
186
  children,
134
187
  style,
@@ -0,0 +1,90 @@
1
+ import React, { useState } from 'react';
2
+ import { View } from 'react-native';
3
+ import type { Meta, StoryObj } from '@storybook/react-vite';
4
+
5
+ import * as TextField from './index';
6
+
7
+ const meta: Meta = {
8
+ title: 'Components/TextField',
9
+ };
10
+
11
+ export default meta;
12
+
13
+ type Story = StoryObj;
14
+
15
+ function ControlledField({
16
+ label,
17
+ placeholder,
18
+ initial = '',
19
+ isInvalid,
20
+ editable = true,
21
+ }: {
22
+ label: string;
23
+ placeholder?: string;
24
+ initial?: string;
25
+ isInvalid?: boolean;
26
+ editable?: boolean;
27
+ }) {
28
+ const [value, setValue] = useState(initial);
29
+ return (
30
+ <View style={{ width: 320 }}>
31
+ <TextField.LabelText>{label}</TextField.LabelText>
32
+ <TextField.Root isInvalid={isInvalid}>
33
+ <TextField.Input
34
+ label={label}
35
+ placeholder={placeholder}
36
+ value={value}
37
+ onChangeText={setValue}
38
+ editable={editable}
39
+ isInvalid={isInvalid}
40
+ />
41
+ </TextField.Root>
42
+ </View>
43
+ );
44
+ }
45
+
46
+ export const Basic: Story = {
47
+ render: () => (
48
+ <ControlledField label="Username" placeholder="oxylander" />
49
+ ),
50
+ };
51
+
52
+ export const WithValue: Story = {
53
+ render: () => (
54
+ <ControlledField label="Email" initial="nate@oxy.so" />
55
+ ),
56
+ };
57
+
58
+ export const Error: Story = {
59
+ render: () => (
60
+ <ControlledField
61
+ label="Email"
62
+ initial="not-an-email"
63
+ isInvalid
64
+ />
65
+ ),
66
+ };
67
+
68
+ export const Disabled: Story = {
69
+ render: () => (
70
+ <ControlledField
71
+ label="Username"
72
+ initial="oxylander"
73
+ editable={false}
74
+ />
75
+ ),
76
+ };
77
+
78
+ export const Composition: Story = {
79
+ render: () => (
80
+ <View style={{ gap: 16 }}>
81
+ <ControlledField label="First name" placeholder="Ada" />
82
+ <ControlledField label="Last name" placeholder="Lovelace" />
83
+ <ControlledField
84
+ label="Email"
85
+ initial="invalid-email"
86
+ isInvalid
87
+ />
88
+ </View>
89
+ ),
90
+ };