@ezpg/app-ui 0.2.0 → 0.4.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 (203) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/lib/module/blocks/brand-profile-card/brand-profile-card.js +2 -2
  3. package/lib/module/blocks/brand-profile-card/brand-profile-card.js.map +1 -1
  4. package/lib/module/blocks/bulk-ship-list/bulk-ship-list.js +1 -1
  5. package/lib/module/blocks/bulk-ship-list/bulk-ship-list.js.map +1 -1
  6. package/lib/module/blocks/claim-row/claim-row.js +6 -5
  7. package/lib/module/blocks/claim-row/claim-row.js.map +1 -1
  8. package/lib/module/blocks/color-swatch-picker/color-swatch-picker.js +1 -1
  9. package/lib/module/blocks/color-swatch-picker/color-swatch-picker.js.map +1 -1
  10. package/lib/module/blocks/copy-variant-picker/copy-variant-picker.js +4 -4
  11. package/lib/module/blocks/copy-variant-picker/copy-variant-picker.js.map +1 -1
  12. package/lib/module/blocks/credit-wallet/credit-wallet.js +1 -1
  13. package/lib/module/blocks/credit-wallet/credit-wallet.js.map +1 -1
  14. package/lib/module/blocks/generation-progress/generation-progress.js +4 -4
  15. package/lib/module/blocks/generation-progress/generation-progress.js.map +1 -1
  16. package/lib/module/blocks/image-grid/image-grid.js +11 -11
  17. package/lib/module/blocks/image-grid/image-grid.js.map +1 -1
  18. package/lib/module/blocks/internal/format.js +4 -0
  19. package/lib/module/blocks/internal/format.js.map +1 -1
  20. package/lib/module/blocks/invoice-row/invoice-row.js +5 -5
  21. package/lib/module/blocks/invoice-row/invoice-row.js.map +1 -1
  22. package/lib/module/blocks/logo-candidate-grid/logo-candidate-grid.js +1 -1
  23. package/lib/module/blocks/logo-candidate-grid/logo-candidate-grid.js.map +1 -1
  24. package/lib/module/blocks/oauth-buttons/oauth-buttons.js +2 -2
  25. package/lib/module/blocks/oauth-buttons/oauth-buttons.js.map +1 -1
  26. package/lib/module/blocks/option-editor/option-editor.js +3 -3
  27. package/lib/module/blocks/option-editor/option-editor.js.map +1 -1
  28. package/lib/module/blocks/order-card/index.js +1 -1
  29. package/lib/module/blocks/order-card/index.js.map +1 -1
  30. package/lib/module/blocks/order-card/order-card.js +31 -9
  31. package/lib/module/blocks/order-card/order-card.js.map +1 -1
  32. package/lib/module/blocks/plan-card/plan-card.js +3 -3
  33. package/lib/module/blocks/plan-card/plan-card.js.map +1 -1
  34. package/lib/module/blocks/product-list-empty/product-list-empty.js +5 -5
  35. package/lib/module/blocks/product-list-empty/product-list-empty.js.map +1 -1
  36. package/lib/module/blocks/review-row/review-row.js +17 -17
  37. package/lib/module/blocks/review-row/review-row.js.map +1 -1
  38. package/lib/module/blocks/sales-chart/sales-chart.js +3 -3
  39. package/lib/module/blocks/sales-chart/sales-chart.js.map +1 -1
  40. package/lib/module/blocks/selection-bar/selection-bar.js +1 -1
  41. package/lib/module/blocks/selection-bar/selection-bar.js.map +1 -1
  42. package/lib/module/blocks/service-state-banner/service-state-banner.js +1 -1
  43. package/lib/module/blocks/service-state-banner/service-state-banner.js.map +1 -1
  44. package/lib/module/blocks/shipping-tracking-row/shipping-tracking-row.js +1 -1
  45. package/lib/module/blocks/shipping-tracking-row/shipping-tracking-row.js.map +1 -1
  46. package/lib/module/blocks/site-preview-frame/site-preview-frame.js +1 -1
  47. package/lib/module/blocks/site-preview-frame/site-preview-frame.js.map +1 -1
  48. package/lib/module/blocks/stat-card/stat-card.js +1 -1
  49. package/lib/module/blocks/stat-card/stat-card.js.map +1 -1
  50. package/lib/module/blocks/step-screen/step-screen.js +18 -14
  51. package/lib/module/blocks/step-screen/step-screen.js.map +1 -1
  52. package/lib/module/blocks/supply-item-card/supply-item-card.js +5 -5
  53. package/lib/module/blocks/supply-item-card/supply-item-card.js.map +1 -1
  54. package/lib/module/components/button/button.js +15 -20
  55. package/lib/module/components/button/button.js.map +1 -1
  56. package/lib/module/components/chip/chip.js +2 -2
  57. package/lib/module/components/chip/chip.js.map +1 -1
  58. package/lib/module/components/dialog/dialog.js +6 -6
  59. package/lib/module/components/dialog/dialog.js.map +1 -1
  60. package/lib/module/components/empty-state/empty-state.js +4 -4
  61. package/lib/module/components/empty-state/empty-state.js.map +1 -1
  62. package/lib/module/components/field/field.js +20 -19
  63. package/lib/module/components/field/field.js.map +1 -1
  64. package/lib/module/components/list-row/index.js.map +1 -1
  65. package/lib/module/components/list-row/list-row.js +38 -10
  66. package/lib/module/components/list-row/list-row.js.map +1 -1
  67. package/lib/module/components/price/price.js +1 -1
  68. package/lib/module/components/price/price.js.map +1 -1
  69. package/lib/module/components/segmented-tabs/segmented-tabs.js +2 -2
  70. package/lib/module/components/segmented-tabs/segmented-tabs.js.map +1 -1
  71. package/lib/module/components/status-badge/status-badge.js +2 -2
  72. package/lib/module/components/status-badge/status-badge.js.map +1 -1
  73. package/lib/module/components/text/text.js +7 -2
  74. package/lib/module/components/text/text.js.map +1 -1
  75. package/lib/module/components/toast/toast.js +5 -5
  76. package/lib/module/components/toast/toast.js.map +1 -1
  77. package/lib/module/index.js +1 -0
  78. package/lib/module/index.js.map +1 -1
  79. package/lib/module/templates/editor-template.js +142 -0
  80. package/lib/module/templates/editor-template.js.map +1 -0
  81. package/lib/module/templates/index.js +9 -0
  82. package/lib/module/templates/index.js.map +1 -0
  83. package/lib/module/templates/list-template.js +98 -0
  84. package/lib/module/templates/list-template.js.map +1 -0
  85. package/lib/module/templates/parts.js +115 -0
  86. package/lib/module/templates/parts.js.map +1 -0
  87. package/lib/module/templates/result-template.js +233 -0
  88. package/lib/module/templates/result-template.js.map +1 -0
  89. package/lib/module/templates/scroll-templates.js +100 -0
  90. package/lib/module/templates/scroll-templates.js.map +1 -0
  91. package/lib/module/templates/system-template.js +64 -0
  92. package/lib/module/templates/system-template.js.map +1 -0
  93. package/lib/module/theme/base.js +141 -0
  94. package/lib/module/theme/base.js.map +1 -0
  95. package/lib/module/theme/contract.js +6 -0
  96. package/lib/module/theme/contract.js.map +1 -0
  97. package/lib/module/theme/tokens.js +18 -114
  98. package/lib/module/theme/tokens.js.map +1 -1
  99. package/lib/typescript/src/blocks/claim-row/claim-row.d.ts.map +1 -1
  100. package/lib/typescript/src/blocks/color-swatch-picker/color-swatch-picker.d.ts.map +1 -1
  101. package/lib/typescript/src/blocks/image-grid/image-grid.d.ts.map +1 -1
  102. package/lib/typescript/src/blocks/internal/format.d.ts +1 -0
  103. package/lib/typescript/src/blocks/internal/format.d.ts.map +1 -1
  104. package/lib/typescript/src/blocks/invoice-row/invoice-row.d.ts.map +1 -1
  105. package/lib/typescript/src/blocks/oauth-buttons/oauth-buttons.d.ts.map +1 -1
  106. package/lib/typescript/src/blocks/order-card/index.d.ts +1 -1
  107. package/lib/typescript/src/blocks/order-card/index.d.ts.map +1 -1
  108. package/lib/typescript/src/blocks/order-card/order-card.d.ts +10 -0
  109. package/lib/typescript/src/blocks/order-card/order-card.d.ts.map +1 -1
  110. package/lib/typescript/src/blocks/product-list-empty/product-list-empty.d.ts.map +1 -1
  111. package/lib/typescript/src/blocks/review-row/review-row.d.ts.map +1 -1
  112. package/lib/typescript/src/blocks/step-screen/step-screen.d.ts +3 -1
  113. package/lib/typescript/src/blocks/step-screen/step-screen.d.ts.map +1 -1
  114. package/lib/typescript/src/components/button/button.d.ts +2 -2
  115. package/lib/typescript/src/components/button/button.d.ts.map +1 -1
  116. package/lib/typescript/src/components/dialog/dialog.d.ts +2 -2
  117. package/lib/typescript/src/components/dialog/dialog.d.ts.map +1 -1
  118. package/lib/typescript/src/components/empty-state/empty-state.d.ts.map +1 -1
  119. package/lib/typescript/src/components/field/field.d.ts.map +1 -1
  120. package/lib/typescript/src/components/list-row/index.d.ts +1 -1
  121. package/lib/typescript/src/components/list-row/index.d.ts.map +1 -1
  122. package/lib/typescript/src/components/list-row/list-row.d.ts +4 -1
  123. package/lib/typescript/src/components/list-row/list-row.d.ts.map +1 -1
  124. package/lib/typescript/src/components/status-badge/status-badge.d.ts +2 -1
  125. package/lib/typescript/src/components/status-badge/status-badge.d.ts.map +1 -1
  126. package/lib/typescript/src/components/text/text.d.ts +1 -1
  127. package/lib/typescript/src/components/text/text.d.ts.map +1 -1
  128. package/lib/typescript/src/components/toast/toast.d.ts +2 -1
  129. package/lib/typescript/src/components/toast/toast.d.ts.map +1 -1
  130. package/lib/typescript/src/index.d.ts +1 -0
  131. package/lib/typescript/src/index.d.ts.map +1 -1
  132. package/lib/typescript/src/templates/editor-template.d.ts +22 -0
  133. package/lib/typescript/src/templates/editor-template.d.ts.map +1 -0
  134. package/lib/typescript/src/templates/index.d.ts +13 -0
  135. package/lib/typescript/src/templates/index.d.ts.map +1 -0
  136. package/lib/typescript/src/templates/list-template.d.ts +27 -0
  137. package/lib/typescript/src/templates/list-template.d.ts.map +1 -0
  138. package/lib/typescript/src/templates/parts.d.ts +27 -0
  139. package/lib/typescript/src/templates/parts.d.ts.map +1 -0
  140. package/lib/typescript/src/templates/result-template.d.ts +21 -0
  141. package/lib/typescript/src/templates/result-template.d.ts.map +1 -0
  142. package/lib/typescript/src/templates/scroll-templates.d.ts +32 -0
  143. package/lib/typescript/src/templates/scroll-templates.d.ts.map +1 -0
  144. package/lib/typescript/src/templates/system-template.d.ts +12 -0
  145. package/lib/typescript/src/templates/system-template.d.ts.map +1 -0
  146. package/lib/typescript/src/theme/base.d.ts +139 -0
  147. package/lib/typescript/src/theme/base.d.ts.map +1 -0
  148. package/lib/typescript/src/theme/contract.d.ts +7 -0
  149. package/lib/typescript/src/theme/contract.d.ts.map +1 -0
  150. package/lib/typescript/src/theme/tokens.d.ts.map +1 -1
  151. package/lib/typescript/src/types/index.d.ts +25 -3
  152. package/lib/typescript/src/types/index.d.ts.map +1 -1
  153. package/package.json +1 -1
  154. package/src/blocks/brand-profile-card/brand-profile-card.tsx +2 -2
  155. package/src/blocks/bulk-ship-list/bulk-ship-list.tsx +1 -1
  156. package/src/blocks/claim-row/claim-row.tsx +12 -3
  157. package/src/blocks/color-swatch-picker/color-swatch-picker.tsx +5 -1
  158. package/src/blocks/copy-variant-picker/copy-variant-picker.tsx +4 -4
  159. package/src/blocks/credit-wallet/credit-wallet.tsx +1 -1
  160. package/src/blocks/generation-progress/generation-progress.tsx +4 -4
  161. package/src/blocks/image-grid/image-grid.tsx +16 -9
  162. package/src/blocks/internal/format.ts +5 -0
  163. package/src/blocks/invoice-row/invoice-row.tsx +8 -4
  164. package/src/blocks/logo-candidate-grid/logo-candidate-grid.tsx +1 -1
  165. package/src/blocks/oauth-buttons/oauth-buttons.tsx +6 -2
  166. package/src/blocks/option-editor/option-editor.tsx +3 -3
  167. package/src/blocks/order-card/index.ts +4 -0
  168. package/src/blocks/order-card/order-card.tsx +50 -6
  169. package/src/blocks/plan-card/plan-card.tsx +3 -3
  170. package/src/blocks/product-list-empty/product-list-empty.tsx +11 -4
  171. package/src/blocks/review-row/review-row.tsx +26 -15
  172. package/src/blocks/sales-chart/sales-chart.tsx +3 -3
  173. package/src/blocks/selection-bar/selection-bar.tsx +1 -1
  174. package/src/blocks/service-state-banner/service-state-banner.tsx +1 -1
  175. package/src/blocks/shipping-tracking-row/shipping-tracking-row.tsx +1 -1
  176. package/src/blocks/site-preview-frame/site-preview-frame.tsx +1 -1
  177. package/src/blocks/stat-card/stat-card.tsx +1 -1
  178. package/src/blocks/step-screen/step-screen.tsx +24 -12
  179. package/src/blocks/supply-item-card/supply-item-card.tsx +5 -5
  180. package/src/components/button/button.tsx +24 -16
  181. package/src/components/chip/chip.tsx +2 -2
  182. package/src/components/dialog/dialog.tsx +7 -7
  183. package/src/components/empty-state/empty-state.tsx +8 -4
  184. package/src/components/field/field.tsx +20 -19
  185. package/src/components/list-row/index.ts +1 -1
  186. package/src/components/list-row/list-row.tsx +35 -8
  187. package/src/components/price/price.tsx +1 -1
  188. package/src/components/segmented-tabs/segmented-tabs.tsx +2 -2
  189. package/src/components/status-badge/status-badge.tsx +4 -4
  190. package/src/components/text/text.tsx +5 -2
  191. package/src/components/toast/toast.tsx +7 -6
  192. package/src/index.tsx +1 -0
  193. package/src/templates/editor-template.tsx +164 -0
  194. package/src/templates/index.ts +26 -0
  195. package/src/templates/list-template.tsx +124 -0
  196. package/src/templates/parts.tsx +138 -0
  197. package/src/templates/result-template.tsx +266 -0
  198. package/src/templates/scroll-templates.tsx +140 -0
  199. package/src/templates/system-template.tsx +70 -0
  200. package/src/theme/base.ts +149 -0
  201. package/src/theme/contract.ts +21 -0
  202. package/src/theme/tokens.ts +30 -81
  203. package/src/types/index.ts +17 -3
@@ -0,0 +1,164 @@
1
+ import type { ReactNode } from 'react';
2
+ import { ScrollView, View } from 'react-native';
3
+ import { Button } from '../components/button/button';
4
+ import { Text } from '../components/text/text';
5
+ import { CloseGlyph } from '../internal/glyphs';
6
+ import { PressableBase } from '../internal/pressable-base';
7
+ import { useTheme } from '../theme/theme-provider';
8
+ import { makeStyles } from '../utils/styles';
9
+
10
+ export type EditorTool = {
11
+ key: string;
12
+ label: string;
13
+ onPress: () => void;
14
+ disabled?: boolean;
15
+ };
16
+
17
+ export type EditorSheet = {
18
+ title: string;
19
+ onClose: () => void;
20
+ headerAction?: ReactNode;
21
+ children: ReactNode;
22
+ };
23
+
24
+ export type EditorTemplateProps = {
25
+ preview: ReactNode;
26
+ tools: readonly EditorTool[];
27
+ sheet?: EditorSheet | null;
28
+ closeLabel?: string;
29
+ bottomInset?: number;
30
+ };
31
+
32
+ const useStyles = makeStyles((t) => ({
33
+ root: { flex: 1, backgroundColor: t.colors.muted },
34
+ canvas: {
35
+ flex: 1,
36
+ paddingTop: t.layout.groupGap,
37
+ paddingHorizontal: t.layout.screenX,
38
+ },
39
+ preview: {
40
+ flex: 1,
41
+ overflow: 'hidden',
42
+ borderTopLeftRadius: t.radius.lg,
43
+ borderTopRightRadius: t.radius.lg,
44
+ backgroundColor: t.colors.background,
45
+ },
46
+ tools: {
47
+ flexDirection: 'row',
48
+ gap: t.layout.itemGap,
49
+ paddingHorizontal: t.layout.screenX,
50
+ paddingTop: t.layout.footerTop,
51
+ borderTopWidth: 1,
52
+ borderTopColor: t.colors.border,
53
+ backgroundColor: t.colors.background,
54
+ },
55
+ tool: { flex: 1 },
56
+ sheet: {
57
+ position: 'absolute',
58
+ left: 0,
59
+ right: 0,
60
+ bottom: 0,
61
+ maxHeight: '60%',
62
+ borderTopLeftRadius: t.radius.lg,
63
+ borderTopRightRadius: t.radius.lg,
64
+ borderTopWidth: 1,
65
+ borderColor: t.colors.border,
66
+ backgroundColor: t.colors.background,
67
+ },
68
+ grab: {
69
+ alignSelf: 'center',
70
+ width: t.space[8],
71
+ height: t.space[1],
72
+ marginTop: t.space[2],
73
+ borderRadius: t.radius.full,
74
+ backgroundColor: t.colors.input,
75
+ },
76
+ sheetHead: {
77
+ flexDirection: 'row',
78
+ alignItems: 'center',
79
+ gap: t.space[2],
80
+ paddingLeft: t.layout.screenX,
81
+ paddingRight: t.space[1],
82
+ minHeight: t.controlSize.md,
83
+ },
84
+ sheetTitle: { flex: 1 },
85
+ close: {
86
+ width: t.minTouchSize,
87
+ height: t.minTouchSize,
88
+ alignItems: 'center',
89
+ justifyContent: 'center',
90
+ },
91
+ sheetBody: {
92
+ gap: t.space[4],
93
+ paddingHorizontal: t.layout.screenX,
94
+ paddingTop: t.space[2],
95
+ },
96
+ }));
97
+
98
+ export function EditorTemplate({
99
+ preview,
100
+ tools,
101
+ sheet,
102
+ closeLabel = '닫기',
103
+ bottomInset = 0,
104
+ }: EditorTemplateProps) {
105
+ const t = useTheme();
106
+ const s = useStyles();
107
+ return (
108
+ <View style={s.root}>
109
+ <View style={s.canvas}>
110
+ <View style={s.preview}>{preview}</View>
111
+ </View>
112
+ <View
113
+ style={[
114
+ s.tools,
115
+ { paddingBottom: t.layout.footerBottom + bottomInset },
116
+ ]}
117
+ >
118
+ {tools.map((tool) => (
119
+ <Button
120
+ key={tool.key}
121
+ variant="outline"
122
+ onPress={tool.onPress}
123
+ disabled={tool.disabled}
124
+ style={s.tool}
125
+ >
126
+ {tool.label}
127
+ </Button>
128
+ ))}
129
+ </View>
130
+ {sheet ? (
131
+ <View style={s.sheet}>
132
+ <View style={s.grab} />
133
+ <View style={s.sheetHead}>
134
+ <Text
135
+ variant="bodyLg"
136
+ weight="semibold"
137
+ accessibilityRole="header"
138
+ style={s.sheetTitle}
139
+ >
140
+ {sheet.title}
141
+ </Text>
142
+ {sheet.headerAction}
143
+ <PressableBase
144
+ onPress={sheet.onClose}
145
+ accessibilityLabel={closeLabel}
146
+ style={s.close}
147
+ >
148
+ <CloseGlyph color={t.colors.foreground} size={t.iconSize.md} />
149
+ </PressableBase>
150
+ </View>
151
+ <ScrollView
152
+ keyboardShouldPersistTaps="handled"
153
+ contentContainerStyle={[
154
+ s.sheetBody,
155
+ { paddingBottom: t.layout.footerBottom + bottomInset },
156
+ ]}
157
+ >
158
+ {sheet.children}
159
+ </ScrollView>
160
+ </View>
161
+ ) : null}
162
+ </View>
163
+ );
164
+ }
@@ -0,0 +1,26 @@
1
+ export {
2
+ DetailTemplate,
3
+ DashboardTemplate,
4
+ FormTemplate,
5
+ MenuTemplate,
6
+ } from './scroll-templates';
7
+ export type {
8
+ DashboardTemplateProps,
9
+ DetailTemplateProps,
10
+ FormTemplateProps,
11
+ MenuTemplateProps,
12
+ } from './scroll-templates';
13
+ export { EditorTemplate } from './editor-template';
14
+ export type {
15
+ EditorSheet,
16
+ EditorTemplateProps,
17
+ EditorTool,
18
+ } from './editor-template';
19
+ export { ListTemplate } from './list-template';
20
+ export type { ListTemplateProps, ListTemplateStatus } from './list-template';
21
+ export { ResultTemplate } from './result-template';
22
+ export type { ResultPreview, ResultTemplateProps } from './result-template';
23
+ export { SystemTemplate } from './system-template';
24
+ export type { SystemTemplateProps } from './system-template';
25
+ export { ListSection } from './parts';
26
+ export type { NoticeReason, TemplateAction, TemplateNotice } from './parts';
@@ -0,0 +1,124 @@
1
+ import type { ReactElement, ReactNode } from 'react';
2
+ import {
3
+ FlatList,
4
+ RefreshControl,
5
+ View,
6
+ type ListRenderItem,
7
+ } from 'react-native';
8
+ import { EmptyState } from '../components/empty-state/empty-state';
9
+ import { Skeleton } from '../components/skeleton/skeleton';
10
+ import { useTheme } from '../theme/theme-provider';
11
+ import { makeStyles } from '../utils/styles';
12
+ import type { TemplateAction } from './parts';
13
+
14
+ export type ListTemplateStatus = 'ready' | 'loading' | 'error';
15
+
16
+ export type ListTemplateProps<T> = {
17
+ data: readonly T[];
18
+ renderItem: ListRenderItem<T>;
19
+ keyExtractor: (item: T) => string;
20
+ status?: ListTemplateStatus;
21
+ filters?: ReactNode;
22
+ pinned?: ReactNode;
23
+ empty: { title: string; action?: Pick<TemplateAction, 'label' | 'onPress'> };
24
+ error?: { title: string; onRetry: () => void };
25
+ refreshing?: boolean;
26
+ onRefresh?: () => void;
27
+ onEndReached?: () => void;
28
+ selectionBar?: ReactElement | null;
29
+ bottomInset?: number;
30
+ };
31
+
32
+ const useStyles = makeStyles((t) => ({
33
+ root: { flex: 1, backgroundColor: t.colors.background },
34
+ filters: {
35
+ gap: t.layout.groupGap,
36
+ paddingTop: t.space[2],
37
+ paddingBottom: t.space[2],
38
+ paddingHorizontal: t.layout.screenX,
39
+ },
40
+ loading: {
41
+ gap: t.space[4],
42
+ paddingTop: t.space[2],
43
+ paddingHorizontal: t.layout.screenX,
44
+ },
45
+ grow: { flexGrow: 1 },
46
+ }));
47
+
48
+ export function ListTemplate<T>({
49
+ data,
50
+ renderItem,
51
+ keyExtractor,
52
+ status = 'ready',
53
+ filters,
54
+ pinned,
55
+ empty,
56
+ error,
57
+ refreshing = false,
58
+ onRefresh,
59
+ onEndReached,
60
+ selectionBar,
61
+ bottomInset = 0,
62
+ }: ListTemplateProps<T>) {
63
+ const t = useTheme();
64
+ const s = useStyles();
65
+ const head = filters ? <View style={s.filters}>{filters}</View> : null;
66
+
67
+ if (status === 'loading') {
68
+ return (
69
+ <View style={s.root}>
70
+ {head}
71
+ <View style={s.loading}>
72
+ {[0, 1, 2, 3, 4].map((i) => (
73
+ <Skeleton key={i} height={t.listRow.oneLine} />
74
+ ))}
75
+ </View>
76
+ </View>
77
+ );
78
+ }
79
+
80
+ if (status === 'error' && error) {
81
+ return (
82
+ <View style={s.root}>
83
+ {head}
84
+ <EmptyState
85
+ variant="error"
86
+ title={error.title}
87
+ action={{ label: '다시 시도', onPress: error.onRetry }}
88
+ />
89
+ </View>
90
+ );
91
+ }
92
+
93
+ return (
94
+ <View style={s.root}>
95
+ <FlatList
96
+ data={data}
97
+ renderItem={renderItem}
98
+ keyExtractor={keyExtractor}
99
+ ListHeaderComponent={
100
+ head || pinned ? (
101
+ <>
102
+ {head}
103
+ {pinned}
104
+ </>
105
+ ) : undefined
106
+ }
107
+ ListEmptyComponent={
108
+ <EmptyState title={empty.title} action={empty.action} />
109
+ }
110
+ contentContainerStyle={[
111
+ data.length === 0 ? s.grow : null,
112
+ selectionBar ? null : { paddingBottom: bottomInset },
113
+ ]}
114
+ refreshControl={
115
+ onRefresh ? (
116
+ <RefreshControl refreshing={refreshing} onRefresh={onRefresh} />
117
+ ) : undefined
118
+ }
119
+ onEndReached={onEndReached}
120
+ />
121
+ {selectionBar}
122
+ </View>
123
+ );
124
+ }
@@ -0,0 +1,138 @@
1
+ import type { ReactNode } from 'react';
2
+ import { View } from 'react-native';
3
+ import { Button } from '../components/button/button';
4
+ import { Text } from '../components/text/text';
5
+ import { AlertGlyph } from '../internal/glyphs';
6
+ import { useTheme } from '../theme/theme-provider';
7
+ import { makeStyles } from '../utils/styles';
8
+
9
+ export type TemplateAction = {
10
+ label: string;
11
+ onPress: () => void;
12
+ disabled?: boolean;
13
+ loading?: boolean;
14
+ };
15
+
16
+ export type NoticeReason = 'error' | 'money' | 'irreversible' | 'legal';
17
+
18
+ export type TemplateNotice = { reason: NoticeReason; text: string };
19
+
20
+ const useStyles = makeStyles((t) => ({
21
+ bar: {
22
+ gap: t.layout.itemGap,
23
+ paddingHorizontal: t.layout.screenX,
24
+ paddingTop: t.layout.footerTop,
25
+ backgroundColor: t.colors.background,
26
+ },
27
+ notice: {
28
+ flexDirection: 'row',
29
+ alignItems: 'flex-start',
30
+ gap: t.space[2],
31
+ marginHorizontal: t.layout.screenX,
32
+ paddingVertical: t.space[3],
33
+ paddingHorizontal: t.space[4],
34
+ borderRadius: t.radius.md,
35
+ backgroundColor: t.colors.muted,
36
+ },
37
+ noticeText: { flex: 1 },
38
+ section: { paddingBottom: t.space[2] },
39
+ sectionHead: {
40
+ flexDirection: 'row',
41
+ alignItems: 'center',
42
+ justifyContent: 'space-between',
43
+ paddingTop: t.space[4],
44
+ paddingBottom: t.space[2],
45
+ paddingHorizontal: t.layout.screenX,
46
+ },
47
+ inset: { paddingHorizontal: t.layout.screenX },
48
+ }));
49
+
50
+ export function BottomBar({
51
+ primaryAction,
52
+ secondaryAction,
53
+ bottomInset = 0,
54
+ }: {
55
+ primaryAction: TemplateAction;
56
+ secondaryAction?: TemplateAction;
57
+ bottomInset?: number;
58
+ }) {
59
+ const t = useTheme();
60
+ const s = useStyles();
61
+ return (
62
+ <View
63
+ style={[s.bar, { paddingBottom: t.layout.footerBottom + bottomInset }]}
64
+ >
65
+ <Button
66
+ size="lg"
67
+ onPress={primaryAction.onPress}
68
+ disabled={primaryAction.disabled}
69
+ loading={primaryAction.loading}
70
+ >
71
+ {primaryAction.label}
72
+ </Button>
73
+ {secondaryAction ? (
74
+ <Button
75
+ variant="ghost"
76
+ onPress={secondaryAction.onPress}
77
+ disabled={secondaryAction.disabled}
78
+ loading={secondaryAction.loading}
79
+ >
80
+ {secondaryAction.label}
81
+ </Button>
82
+ ) : null}
83
+ </View>
84
+ );
85
+ }
86
+
87
+ export function Notice({ notice }: { notice: TemplateNotice }) {
88
+ const t = useTheme();
89
+ const s = useStyles();
90
+ const danger = notice.reason === 'error';
91
+ return (
92
+ <View accessibilityRole={danger ? 'alert' : undefined} style={s.notice}>
93
+ <AlertGlyph
94
+ color={danger ? t.colors.danger : t.colors.mutedForeground}
95
+ size={t.iconSize.md}
96
+ />
97
+ <Text
98
+ variant="label"
99
+ tone={danger ? 'danger' : 'default'}
100
+ style={s.noticeText}
101
+ >
102
+ {notice.text}
103
+ </Text>
104
+ </View>
105
+ );
106
+ }
107
+
108
+ export function ListSection({
109
+ title,
110
+ action,
111
+ inset,
112
+ children,
113
+ }: {
114
+ title?: string;
115
+ action?: ReactNode;
116
+ inset?: boolean;
117
+ children: ReactNode;
118
+ }) {
119
+ const s = useStyles();
120
+ return (
121
+ <View style={s.section}>
122
+ {title ? (
123
+ <View style={s.sectionHead}>
124
+ <Text
125
+ variant="label"
126
+ tone="muted"
127
+ weight="semibold"
128
+ accessibilityRole="header"
129
+ >
130
+ {title}
131
+ </Text>
132
+ {action}
133
+ </View>
134
+ ) : null}
135
+ {inset ? <View style={s.inset}>{children}</View> : children}
136
+ </View>
137
+ );
138
+ }
@@ -0,0 +1,266 @@
1
+ import { useState, type ReactNode } from 'react';
2
+ import {
3
+ Modal,
4
+ ScrollView,
5
+ View,
6
+ type LayoutChangeEvent,
7
+ type NativeScrollEvent,
8
+ type NativeSyntheticEvent,
9
+ } from 'react-native';
10
+ import { Text } from '../components/text/text';
11
+ import { CloseGlyph } from '../internal/glyphs';
12
+ import { PressableBase } from '../internal/pressable-base';
13
+ import { useTheme } from '../theme/theme-provider';
14
+ import { makeStyles } from '../utils/styles';
15
+ import {
16
+ BottomBar,
17
+ Notice,
18
+ type TemplateAction,
19
+ type TemplateNotice,
20
+ } from './parts';
21
+
22
+ export type ResultPreview = {
23
+ key: string;
24
+ label: string;
25
+ render: () => ReactNode;
26
+ };
27
+
28
+ export type ResultTemplateProps = {
29
+ previews: readonly ResultPreview[];
30
+ index: number;
31
+ onIndexChange: (index: number) => void;
32
+ progress?: ReactNode;
33
+ notice?: TemplateNotice;
34
+ primaryAction?: TemplateAction;
35
+ secondaryAction?: TemplateAction;
36
+ openLabel?: string;
37
+ closeLabel?: string;
38
+ bottomInset?: number;
39
+ };
40
+
41
+ const useStyles = makeStyles((t) => ({
42
+ root: { flex: 1, backgroundColor: t.colors.background },
43
+ body: { flex: 1, paddingTop: t.space[2] },
44
+ pager: { flex: 1 },
45
+ page: { gap: t.space[2] },
46
+ card: {
47
+ flex: 1,
48
+ overflow: 'hidden',
49
+ borderRadius: t.radius.lg,
50
+ borderWidth: 1,
51
+ borderColor: t.colors.border,
52
+ backgroundColor: t.colors.muted,
53
+ },
54
+ caption: {
55
+ flexDirection: 'row',
56
+ justifyContent: 'space-between',
57
+ alignItems: 'center',
58
+ },
59
+ dots: {
60
+ flexDirection: 'row',
61
+ justifyContent: 'center',
62
+ gap: t.space[2],
63
+ paddingVertical: t.layout.groupGap,
64
+ },
65
+ dot: {
66
+ width: t.space[2],
67
+ height: t.space[2],
68
+ borderRadius: t.radius.full,
69
+ backgroundColor: t.colors.input,
70
+ },
71
+ dotOn: { width: t.space[5], backgroundColor: t.colors.foreground },
72
+ progress: { flex: 1, paddingHorizontal: t.layout.screenX },
73
+ notice: { paddingBottom: t.layout.groupGap },
74
+ viewer: { flex: 1, backgroundColor: t.colors.background },
75
+ viewerHead: {
76
+ height: t.headerHeight,
77
+ flexDirection: 'row',
78
+ alignItems: 'center',
79
+ paddingHorizontal: t.space[1],
80
+ gap: t.space[1],
81
+ },
82
+ close: {
83
+ width: t.minTouchSize,
84
+ height: t.minTouchSize,
85
+ alignItems: 'center',
86
+ justifyContent: 'center',
87
+ },
88
+ full: { flex: 1 },
89
+ }));
90
+
91
+ function Viewer({
92
+ previews,
93
+ index,
94
+ onIndexChange,
95
+ onClose,
96
+ primaryAction,
97
+ closeLabel,
98
+ bottomInset,
99
+ }: {
100
+ previews: readonly ResultPreview[];
101
+ index: number;
102
+ onIndexChange: (index: number) => void;
103
+ onClose: () => void;
104
+ primaryAction?: TemplateAction;
105
+ closeLabel: string;
106
+ bottomInset: number;
107
+ }) {
108
+ const t = useTheme();
109
+ const s = useStyles();
110
+ const [width, setWidth] = useState(0);
111
+ const current = previews[index];
112
+ return (
113
+ <View style={s.viewer}>
114
+ <View style={s.viewerHead}>
115
+ <PressableBase
116
+ onPress={onClose}
117
+ accessibilityLabel={closeLabel}
118
+ style={s.close}
119
+ >
120
+ <CloseGlyph color={t.colors.foreground} size={t.iconSize.md} />
121
+ </PressableBase>
122
+ <Text variant="bodyLg" weight="semibold" accessibilityRole="header">
123
+ {current
124
+ ? `${current.label} · ${index + 1} / ${previews.length}`
125
+ : ''}
126
+ </Text>
127
+ </View>
128
+ <ScrollView
129
+ horizontal
130
+ pagingEnabled
131
+ showsHorizontalScrollIndicator={false}
132
+ onLayout={(e: LayoutChangeEvent) =>
133
+ setWidth(e.nativeEvent.layout.width)
134
+ }
135
+ contentOffset={{ x: index * width, y: 0 }}
136
+ onMomentumScrollEnd={(e: NativeSyntheticEvent<NativeScrollEvent>) =>
137
+ width &&
138
+ onIndexChange(Math.round(e.nativeEvent.contentOffset.x / width))
139
+ }
140
+ style={s.full}
141
+ >
142
+ {previews.map((preview) => (
143
+ <View key={preview.key} style={{ width }}>
144
+ {preview.render()}
145
+ </View>
146
+ ))}
147
+ </ScrollView>
148
+ {primaryAction ? (
149
+ <BottomBar primaryAction={primaryAction} bottomInset={bottomInset} />
150
+ ) : null}
151
+ </View>
152
+ );
153
+ }
154
+
155
+ export function ResultTemplate({
156
+ previews,
157
+ index,
158
+ onIndexChange,
159
+ progress,
160
+ notice,
161
+ primaryAction,
162
+ secondaryAction,
163
+ openLabel = '크게 보기',
164
+ closeLabel = '닫기',
165
+ bottomInset = 0,
166
+ }: ResultTemplateProps) {
167
+ const t = useTheme();
168
+ const s = useStyles();
169
+ const [width, setWidth] = useState(0);
170
+ const [open, setOpen] = useState(false);
171
+ const page = Math.max(0, width - t.layout.screenX * 2 - t.space[6]);
172
+ const step = page + t.layout.itemGap;
173
+
174
+ return (
175
+ <View style={s.root}>
176
+ <View
177
+ style={s.body}
178
+ onLayout={(e: LayoutChangeEvent) =>
179
+ setWidth(e.nativeEvent.layout.width)
180
+ }
181
+ >
182
+ {progress ? (
183
+ <View style={s.progress}>{progress}</View>
184
+ ) : (
185
+ <>
186
+ <ScrollView
187
+ horizontal
188
+ showsHorizontalScrollIndicator={false}
189
+ snapToInterval={step || undefined}
190
+ decelerationRate="fast"
191
+ contentContainerStyle={{
192
+ gap: t.layout.itemGap,
193
+ paddingHorizontal: t.layout.screenX,
194
+ }}
195
+ onMomentumScrollEnd={(
196
+ e: NativeSyntheticEvent<NativeScrollEvent>
197
+ ) =>
198
+ step &&
199
+ onIndexChange(Math.round(e.nativeEvent.contentOffset.x / step))
200
+ }
201
+ style={s.pager}
202
+ >
203
+ {previews.map((preview, i) => (
204
+ <View key={preview.key} style={[s.page, { width: page }]}>
205
+ <PressableBase
206
+ onPress={() => {
207
+ onIndexChange(i);
208
+ setOpen(true);
209
+ }}
210
+ accessibilityLabel={`${preview.label} ${openLabel}`}
211
+ style={s.card}
212
+ >
213
+ {preview.render()}
214
+ </PressableBase>
215
+ <View style={s.caption}>
216
+ <Text variant="label" weight="semibold">
217
+ {preview.label}
218
+ </Text>
219
+ <Text variant="label" tone="muted">
220
+ {openLabel}
221
+ </Text>
222
+ </View>
223
+ </View>
224
+ ))}
225
+ </ScrollView>
226
+ <View style={s.dots}>
227
+ {previews.map((preview, i) => (
228
+ <View
229
+ key={preview.key}
230
+ style={[s.dot, i === index ? s.dotOn : null]}
231
+ />
232
+ ))}
233
+ </View>
234
+ </>
235
+ )}
236
+ </View>
237
+ {notice ? (
238
+ <View style={s.notice}>
239
+ <Notice notice={notice} />
240
+ </View>
241
+ ) : null}
242
+ {primaryAction && !progress ? (
243
+ <BottomBar
244
+ primaryAction={primaryAction}
245
+ secondaryAction={secondaryAction}
246
+ bottomInset={bottomInset}
247
+ />
248
+ ) : null}
249
+ <Modal
250
+ visible={open}
251
+ animationType="slide"
252
+ onRequestClose={() => setOpen(false)}
253
+ >
254
+ <Viewer
255
+ previews={previews}
256
+ index={index}
257
+ onIndexChange={onIndexChange}
258
+ onClose={() => setOpen(false)}
259
+ primaryAction={primaryAction}
260
+ closeLabel={closeLabel}
261
+ bottomInset={bottomInset}
262
+ />
263
+ </Modal>
264
+ </View>
265
+ );
266
+ }