@openiot-devteam/layout 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +147 -0
- package/dist/dim-bridge.d.mts +21 -0
- package/dist/dim-bridge.d.mts.map +1 -0
- package/dist/dim-bridge.mjs +22 -0
- package/dist/dim-bridge.mjs.map +1 -0
- package/dist/index.d.mts +7 -0
- package/dist/index.mjs +6 -0
- package/dist/native/FitLayout.d.mts +26 -0
- package/dist/native/FitLayout.d.mts.map +1 -0
- package/dist/native/FitLayout.mjs +80 -0
- package/dist/native/FitLayout.mjs.map +1 -0
- package/dist/native/ScreenLayout.d.mts +42 -0
- package/dist/native/ScreenLayout.d.mts.map +1 -0
- package/dist/native/ScreenLayout.mjs +90 -0
- package/dist/native/ScreenLayout.mjs.map +1 -0
- package/dist/native/overlays.d.mts +43 -0
- package/dist/native/overlays.d.mts.map +1 -0
- package/dist/native/overlays.mjs +170 -0
- package/dist/native/overlays.mjs.map +1 -0
- package/dist/native.d.mts +7 -0
- package/dist/native.mjs +6 -0
- package/dist/tokens.d.mts +43 -0
- package/dist/tokens.d.mts.map +1 -0
- package/dist/tokens.mjs +50 -0
- package/dist/tokens.mjs.map +1 -0
- package/dist/types.d.mts +91 -0
- package/dist/types.d.mts.map +1 -0
- package/dist/web/FitLayout.d.mts +55 -0
- package/dist/web/FitLayout.d.mts.map +1 -0
- package/dist/web/FitLayout.mjs +111 -0
- package/dist/web/FitLayout.mjs.map +1 -0
- package/dist/web/ScreenLayout.d.mts +61 -0
- package/dist/web/ScreenLayout.d.mts.map +1 -0
- package/dist/web/ScreenLayout.mjs +118 -0
- package/dist/web/ScreenLayout.mjs.map +1 -0
- package/dist/web/overlays.d.mts +58 -0
- package/dist/web/overlays.d.mts.map +1 -0
- package/dist/web/overlays.mjs +173 -0
- package/dist/web/overlays.mjs.map +1 -0
- package/package.json +63 -0
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { LAYOUT } from "../tokens.mjs";
|
|
2
|
+
import { acquireDim, releaseDim } from "../dim-bridge.mjs";
|
|
3
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
4
|
+
import { useEffect } from "react";
|
|
5
|
+
import { Modal, Pressable, StyleSheet, Text, View } from "react-native";
|
|
6
|
+
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
7
|
+
//#region src/native/overlays.tsx
|
|
8
|
+
/**
|
|
9
|
+
* 네이티브 앱에서는 딤을 이 Modal 이 직접 그리므로 브릿지 호출이 필요 없지만,
|
|
10
|
+
* 웹뷰 셸이 자기 화면 위에 RN 오버레이를 겹칠 때를 위해 신호는 동일하게 올린다.
|
|
11
|
+
*/
|
|
12
|
+
function useDim(open) {
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
if (!open) return;
|
|
15
|
+
acquireDim();
|
|
16
|
+
return releaseDim;
|
|
17
|
+
}, [open]);
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* 아래에서 올라오는 시트 (RN) — 웹의 `BottomSheet` 와 같은 prop 계약.
|
|
21
|
+
*
|
|
22
|
+
* 안드로이드 하드웨어 back 은 `onRequestClose` 로 처리된다.
|
|
23
|
+
*/
|
|
24
|
+
function BottomSheet({ open, onOpenChange, dismissible = true, label, children, style }) {
|
|
25
|
+
const insets = useSafeAreaInsets();
|
|
26
|
+
useDim(open);
|
|
27
|
+
const close = () => onOpenChange(false);
|
|
28
|
+
return /* @__PURE__ */ jsxs(Modal, {
|
|
29
|
+
visible: open,
|
|
30
|
+
transparent: true,
|
|
31
|
+
animationType: "slide",
|
|
32
|
+
statusBarTranslucent: true,
|
|
33
|
+
onRequestClose: close,
|
|
34
|
+
children: [/* @__PURE__ */ jsx(Pressable, {
|
|
35
|
+
style: styles.dim,
|
|
36
|
+
accessibilityLabel: "닫기",
|
|
37
|
+
onPress: dismissible ? close : void 0
|
|
38
|
+
}), /* @__PURE__ */ jsx(View, {
|
|
39
|
+
style: styles.sheetAnchor,
|
|
40
|
+
pointerEvents: "box-none",
|
|
41
|
+
children: /* @__PURE__ */ jsx(View, {
|
|
42
|
+
accessibilityViewIsModal: true,
|
|
43
|
+
accessibilityLabel: label,
|
|
44
|
+
style: [
|
|
45
|
+
styles.sheet,
|
|
46
|
+
{ paddingBottom: 24 + insets.bottom },
|
|
47
|
+
style
|
|
48
|
+
],
|
|
49
|
+
children
|
|
50
|
+
})
|
|
51
|
+
})]
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* 중앙 다이얼로그 (RN) — 웹의 `Dialog` 와 같은 prop 계약.
|
|
56
|
+
*
|
|
57
|
+
* 기본 버튼은 구조만 제공한다. 색·타이포는 앱의 버튼 컴포넌트를 `children` 으로 넣어
|
|
58
|
+
* 맞추는 것을 권장한다.
|
|
59
|
+
*/
|
|
60
|
+
function Dialog({ open, onOpenChange, dismissible = true, title, description, cancel, confirm, children, style }) {
|
|
61
|
+
useDim(open);
|
|
62
|
+
const close = () => onOpenChange(false);
|
|
63
|
+
return /* @__PURE__ */ jsxs(Modal, {
|
|
64
|
+
visible: open,
|
|
65
|
+
transparent: true,
|
|
66
|
+
animationType: "fade",
|
|
67
|
+
statusBarTranslucent: true,
|
|
68
|
+
onRequestClose: close,
|
|
69
|
+
children: [/* @__PURE__ */ jsx(Pressable, {
|
|
70
|
+
style: styles.dim,
|
|
71
|
+
accessibilityLabel: "닫기",
|
|
72
|
+
onPress: dismissible ? close : void 0
|
|
73
|
+
}), /* @__PURE__ */ jsx(View, {
|
|
74
|
+
style: styles.dialogAnchor,
|
|
75
|
+
pointerEvents: "box-none",
|
|
76
|
+
children: /* @__PURE__ */ jsxs(View, {
|
|
77
|
+
accessibilityViewIsModal: true,
|
|
78
|
+
style: [styles.dialog, style],
|
|
79
|
+
children: [
|
|
80
|
+
/* @__PURE__ */ jsx(Text, {
|
|
81
|
+
style: styles.title,
|
|
82
|
+
children: title
|
|
83
|
+
}),
|
|
84
|
+
typeof description === "string" ? /* @__PURE__ */ jsx(Text, {
|
|
85
|
+
style: styles.description,
|
|
86
|
+
children: description
|
|
87
|
+
}) : description,
|
|
88
|
+
children ?? /* @__PURE__ */ jsxs(View, {
|
|
89
|
+
style: styles.buttons,
|
|
90
|
+
children: [cancel && /* @__PURE__ */ jsx(Pressable, {
|
|
91
|
+
style: styles.button,
|
|
92
|
+
accessibilityRole: "button",
|
|
93
|
+
onPress: () => {
|
|
94
|
+
cancel.onPress?.();
|
|
95
|
+
close();
|
|
96
|
+
},
|
|
97
|
+
children: /* @__PURE__ */ jsx(Text, { children: cancel.label })
|
|
98
|
+
}), confirm && /* @__PURE__ */ jsx(Pressable, {
|
|
99
|
+
style: styles.button,
|
|
100
|
+
accessibilityRole: "button",
|
|
101
|
+
onPress: () => {
|
|
102
|
+
confirm.onPress?.();
|
|
103
|
+
close();
|
|
104
|
+
},
|
|
105
|
+
children: /* @__PURE__ */ jsx(Text, { children: confirm.label })
|
|
106
|
+
})]
|
|
107
|
+
})
|
|
108
|
+
]
|
|
109
|
+
})
|
|
110
|
+
})]
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
const styles = StyleSheet.create({
|
|
114
|
+
dim: {
|
|
115
|
+
...StyleSheet.absoluteFillObject,
|
|
116
|
+
backgroundColor: `rgba(0,0,0,${LAYOUT.dimOpacity})`
|
|
117
|
+
},
|
|
118
|
+
sheetAnchor: {
|
|
119
|
+
...StyleSheet.absoluteFillObject,
|
|
120
|
+
justifyContent: "flex-end"
|
|
121
|
+
},
|
|
122
|
+
sheet: {
|
|
123
|
+
width: "100%",
|
|
124
|
+
borderTopLeftRadius: LAYOUT.sheetRadius,
|
|
125
|
+
borderTopRightRadius: LAYOUT.sheetRadius,
|
|
126
|
+
paddingHorizontal: LAYOUT.content.paddingX,
|
|
127
|
+
paddingTop: 12,
|
|
128
|
+
gap: 16
|
|
129
|
+
},
|
|
130
|
+
dialogAnchor: {
|
|
131
|
+
...StyleSheet.absoluteFillObject,
|
|
132
|
+
justifyContent: "center",
|
|
133
|
+
alignItems: "center",
|
|
134
|
+
paddingHorizontal: LAYOUT.dialog.insetX
|
|
135
|
+
},
|
|
136
|
+
dialog: {
|
|
137
|
+
width: "100%",
|
|
138
|
+
borderRadius: LAYOUT.dialog.radius,
|
|
139
|
+
paddingHorizontal: 22,
|
|
140
|
+
paddingTop: 26,
|
|
141
|
+
paddingBottom: 18,
|
|
142
|
+
gap: 12
|
|
143
|
+
},
|
|
144
|
+
title: {
|
|
145
|
+
fontSize: 18,
|
|
146
|
+
fontWeight: "700",
|
|
147
|
+
textAlign: "center"
|
|
148
|
+
},
|
|
149
|
+
description: {
|
|
150
|
+
fontSize: 14,
|
|
151
|
+
lineHeight: 21,
|
|
152
|
+
textAlign: "center"
|
|
153
|
+
},
|
|
154
|
+
buttons: {
|
|
155
|
+
flexDirection: "row",
|
|
156
|
+
gap: 10,
|
|
157
|
+
marginTop: 6
|
|
158
|
+
},
|
|
159
|
+
button: {
|
|
160
|
+
flex: 1,
|
|
161
|
+
height: 48,
|
|
162
|
+
alignItems: "center",
|
|
163
|
+
justifyContent: "center",
|
|
164
|
+
borderRadius: 10
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
//#endregion
|
|
168
|
+
export { BottomSheet, Dialog };
|
|
169
|
+
|
|
170
|
+
//# sourceMappingURL=overlays.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"overlays.mjs","names":[],"sources":["../../src/native/overlays.tsx"],"sourcesContent":["import { useEffect } from 'react'\nimport {\n Modal,\n Pressable,\n StyleSheet,\n Text,\n View,\n type StyleProp,\n type ViewStyle,\n} from 'react-native'\nimport { useSafeAreaInsets } from 'react-native-safe-area-context'\nimport { acquireDim, releaseDim } from '../dim-bridge.js'\nimport { LAYOUT } from '../tokens.js'\nimport type { BottomSheetBaseProps, DialogBaseProps } from '../types.js'\n\n/**\n * 네이티브 앱에서는 딤을 이 Modal 이 직접 그리므로 브릿지 호출이 필요 없지만,\n * 웹뷰 셸이 자기 화면 위에 RN 오버레이를 겹칠 때를 위해 신호는 동일하게 올린다.\n */\nfunction useDim(open: boolean) {\n useEffect(() => {\n if (!open) return\n acquireDim()\n return releaseDim\n }, [open])\n}\n\nexport interface BottomSheetProps extends BottomSheetBaseProps {\n style?: StyleProp<ViewStyle>\n}\n\n/**\n * 아래에서 올라오는 시트 (RN) — 웹의 `BottomSheet` 와 같은 prop 계약.\n *\n * 안드로이드 하드웨어 back 은 `onRequestClose` 로 처리된다.\n */\nexport function BottomSheet({\n open,\n onOpenChange,\n dismissible = true,\n label,\n children,\n style,\n}: BottomSheetProps) {\n const insets = useSafeAreaInsets()\n useDim(open)\n\n const close = () => onOpenChange(false)\n\n return (\n <Modal\n visible={open}\n transparent\n animationType=\"slide\"\n statusBarTranslucent\n onRequestClose={close}\n >\n <Pressable\n style={styles.dim}\n accessibilityLabel=\"닫기\"\n onPress={dismissible ? close : undefined}\n />\n <View style={styles.sheetAnchor} pointerEvents=\"box-none\">\n <View\n accessibilityViewIsModal\n accessibilityLabel={label}\n style={[styles.sheet, { paddingBottom: 24 + insets.bottom }, style]}\n >\n {children}\n </View>\n </View>\n </Modal>\n )\n}\n\nexport interface DialogProps extends DialogBaseProps {\n style?: StyleProp<ViewStyle>\n}\n\n/**\n * 중앙 다이얼로그 (RN) — 웹의 `Dialog` 와 같은 prop 계약.\n *\n * 기본 버튼은 구조만 제공한다. 색·타이포는 앱의 버튼 컴포넌트를 `children` 으로 넣어\n * 맞추는 것을 권장한다.\n */\nexport function Dialog({\n open,\n onOpenChange,\n dismissible = true,\n title,\n description,\n cancel,\n confirm,\n children,\n style,\n}: DialogProps) {\n useDim(open)\n\n const close = () => onOpenChange(false)\n\n return (\n <Modal\n visible={open}\n transparent\n animationType=\"fade\"\n statusBarTranslucent\n onRequestClose={close}\n >\n <Pressable\n style={styles.dim}\n accessibilityLabel=\"닫기\"\n onPress={dismissible ? close : undefined}\n />\n <View style={styles.dialogAnchor} pointerEvents=\"box-none\">\n <View accessibilityViewIsModal style={[styles.dialog, style]}>\n <Text style={styles.title}>{title}</Text>\n {typeof description === 'string' ? (\n <Text style={styles.description}>{description}</Text>\n ) : (\n description\n )}\n\n {children ?? (\n <View style={styles.buttons}>\n {cancel && (\n <Pressable\n style={styles.button}\n accessibilityRole=\"button\"\n onPress={() => {\n cancel.onPress?.()\n close()\n }}\n >\n <Text>{cancel.label}</Text>\n </Pressable>\n )}\n {confirm && (\n <Pressable\n style={styles.button}\n accessibilityRole=\"button\"\n onPress={() => {\n confirm.onPress?.()\n close()\n }}\n >\n <Text>{confirm.label}</Text>\n </Pressable>\n )}\n </View>\n )}\n </View>\n </View>\n </Modal>\n )\n}\n\nconst styles = StyleSheet.create({\n dim: { ...StyleSheet.absoluteFillObject, backgroundColor: `rgba(0,0,0,${LAYOUT.dimOpacity})` },\n sheetAnchor: { ...StyleSheet.absoluteFillObject, justifyContent: 'flex-end' },\n sheet: {\n width: '100%',\n borderTopLeftRadius: LAYOUT.sheetRadius,\n borderTopRightRadius: LAYOUT.sheetRadius,\n paddingHorizontal: LAYOUT.content.paddingX,\n paddingTop: 12,\n gap: 16,\n },\n dialogAnchor: {\n ...StyleSheet.absoluteFillObject,\n justifyContent: 'center',\n alignItems: 'center',\n paddingHorizontal: LAYOUT.dialog.insetX,\n },\n dialog: {\n width: '100%',\n borderRadius: LAYOUT.dialog.radius,\n paddingHorizontal: 22,\n paddingTop: 26,\n paddingBottom: 18,\n gap: 12,\n },\n title: { fontSize: 18, fontWeight: '700', textAlign: 'center' },\n description: { fontSize: 14, lineHeight: 21, textAlign: 'center' },\n buttons: { flexDirection: 'row', gap: 10, marginTop: 6 },\n button: { flex: 1, height: 48, alignItems: 'center', justifyContent: 'center', borderRadius: 10 },\n})\n"],"mappings":"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"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { BottomSheetBaseProps, DialogBaseProps, FitLayoutBaseProps, FitRegion, OverlayBaseProps, ScreenLayoutBaseProps } from "./types.mjs";
|
|
2
|
+
import { LAYOUT, LayoutTokens } from "./tokens.mjs";
|
|
3
|
+
import { setDimHandler } from "./dim-bridge.mjs";
|
|
4
|
+
import { ScreenLayout, ScreenLayoutProps, Stack, StackProps } from "./native/ScreenLayout.mjs";
|
|
5
|
+
import { FitLayout, FitLayoutProps } from "./native/FitLayout.mjs";
|
|
6
|
+
import { BottomSheet, BottomSheetProps, Dialog, DialogProps } from "./native/overlays.mjs";
|
|
7
|
+
export { BottomSheet, type BottomSheetBaseProps, type BottomSheetProps, Dialog, type DialogBaseProps, type DialogProps, FitLayout, type FitLayoutBaseProps, type FitLayoutProps, type FitRegion, LAYOUT, type LayoutTokens, type OverlayBaseProps, ScreenLayout, type ScreenLayoutBaseProps, type ScreenLayoutProps, Stack, type StackProps, setDimHandler };
|
package/dist/native.mjs
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { LAYOUT } from "./tokens.mjs";
|
|
2
|
+
import { setDimHandler } from "./dim-bridge.mjs";
|
|
3
|
+
import { ScreenLayout, Stack } from "./native/ScreenLayout.mjs";
|
|
4
|
+
import { FitLayout } from "./native/FitLayout.mjs";
|
|
5
|
+
import { BottomSheet, Dialog } from "./native/overlays.mjs";
|
|
6
|
+
export { BottomSheet, Dialog, FitLayout, LAYOUT, ScreenLayout, Stack, setDimHandler };
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
//#region src/tokens.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* 레이아웃 수치. 사내 앱 6곳(openiot-203-webview · openiot-008-app · 202-app-client ·
|
|
4
|
+
* 203-karking-guest-app · openiot-203-app · openiot-sample-app-react-native)에서
|
|
5
|
+
* 반복 등장하던 값을 한 곳으로 모은 것이다.
|
|
6
|
+
*
|
|
7
|
+
* 웹과 RN 컴포넌트가 **같은 상수를 읽어** 두 플랫폼이 갈라지지 않게 한다.
|
|
8
|
+
* 지금까지는 같은 숫자가 각 레포에 복사돼 있어서 조용히 어긋날 수 있었다.
|
|
9
|
+
*/
|
|
10
|
+
declare const LAYOUT: {
|
|
11
|
+
/** 모바일 콘텐츠 최대 폭. 데스크톱 브라우저에서 가운데 정렬 기준. */readonly maxWidth: 428; /** 헤더. 높이는 최소값이며 내용에 따라 늘어난다(2단 헤더 등). */
|
|
12
|
+
readonly header: {
|
|
13
|
+
readonly minHeight: 40;
|
|
14
|
+
}; /** 본문 — px 20 / pt 24 / pb 32 / 섹션 간 gap 48. */
|
|
15
|
+
readonly content: {
|
|
16
|
+
readonly paddingX: 20;
|
|
17
|
+
readonly paddingTop: 24;
|
|
18
|
+
readonly paddingBottom: 32;
|
|
19
|
+
readonly gap: 48;
|
|
20
|
+
}; /** 하단 액션 영역 (CTA 버튼 한 줄). */
|
|
21
|
+
readonly footer: {
|
|
22
|
+
readonly height: 96;
|
|
23
|
+
readonly paddingX: 20;
|
|
24
|
+
readonly paddingY: 24;
|
|
25
|
+
}; /** 탭바 — 웹은 고정 60, RN 은 68 + safe-area. */
|
|
26
|
+
readonly tabBar: {
|
|
27
|
+
readonly web: 60;
|
|
28
|
+
readonly native: 68;
|
|
29
|
+
}; /** 섹션 묶음 간격 (구 ContentLayout). */
|
|
30
|
+
readonly stackGap: 16; /** 바텀시트 상단 라운드. */
|
|
31
|
+
readonly sheetRadius: 20; /** 다이얼로그 — 라운드와 좌우 여백. */
|
|
32
|
+
readonly dialog: {
|
|
33
|
+
readonly radius: 16;
|
|
34
|
+
readonly insetX: 32;
|
|
35
|
+
}; /** FitLayout 의 sheet 구획 상단 라운드. */
|
|
36
|
+
readonly fitSheetRadius: 16; /** 오버레이 딤 불투명도. */
|
|
37
|
+
readonly dimOpacity: 0.62;
|
|
38
|
+
};
|
|
39
|
+
/** 로고·브랜드 규칙과 무관한 순수 구조 수치라 색상은 포함하지 않는다. */
|
|
40
|
+
type LayoutTokens = typeof LAYOUT;
|
|
41
|
+
//#endregion
|
|
42
|
+
export { LAYOUT, LayoutTokens };
|
|
43
|
+
//# sourceMappingURL=tokens.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tokens.d.mts","names":[],"sources":["../src/tokens.ts"],"mappings":";;AAQA;;;;;;;cAAa,MAAA;;;;;;;;;;;;;;;;;;;KAiC2B;EAAA;;;;;;;;;;KAA5B,YAAA,UAAsB,MAAM"}
|
package/dist/tokens.mjs
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
//#region src/tokens.ts
|
|
2
|
+
/**
|
|
3
|
+
* 레이아웃 수치. 사내 앱 6곳(openiot-203-webview · openiot-008-app · 202-app-client ·
|
|
4
|
+
* 203-karking-guest-app · openiot-203-app · openiot-sample-app-react-native)에서
|
|
5
|
+
* 반복 등장하던 값을 한 곳으로 모은 것이다.
|
|
6
|
+
*
|
|
7
|
+
* 웹과 RN 컴포넌트가 **같은 상수를 읽어** 두 플랫폼이 갈라지지 않게 한다.
|
|
8
|
+
* 지금까지는 같은 숫자가 각 레포에 복사돼 있어서 조용히 어긋날 수 있었다.
|
|
9
|
+
*/
|
|
10
|
+
const LAYOUT = {
|
|
11
|
+
/** 모바일 콘텐츠 최대 폭. 데스크톱 브라우저에서 가운데 정렬 기준. */
|
|
12
|
+
maxWidth: 428,
|
|
13
|
+
/** 헤더. 높이는 최소값이며 내용에 따라 늘어난다(2단 헤더 등). */
|
|
14
|
+
header: { minHeight: 40 },
|
|
15
|
+
/** 본문 — px 20 / pt 24 / pb 32 / 섹션 간 gap 48. */
|
|
16
|
+
content: {
|
|
17
|
+
paddingX: 20,
|
|
18
|
+
paddingTop: 24,
|
|
19
|
+
paddingBottom: 32,
|
|
20
|
+
gap: 48
|
|
21
|
+
},
|
|
22
|
+
/** 하단 액션 영역 (CTA 버튼 한 줄). */
|
|
23
|
+
footer: {
|
|
24
|
+
height: 96,
|
|
25
|
+
paddingX: 20,
|
|
26
|
+
paddingY: 24
|
|
27
|
+
},
|
|
28
|
+
/** 탭바 — 웹은 고정 60, RN 은 68 + safe-area. */
|
|
29
|
+
tabBar: {
|
|
30
|
+
web: 60,
|
|
31
|
+
native: 68
|
|
32
|
+
},
|
|
33
|
+
/** 섹션 묶음 간격 (구 ContentLayout). */
|
|
34
|
+
stackGap: 16,
|
|
35
|
+
/** 바텀시트 상단 라운드. */
|
|
36
|
+
sheetRadius: 20,
|
|
37
|
+
/** 다이얼로그 — 라운드와 좌우 여백. */
|
|
38
|
+
dialog: {
|
|
39
|
+
radius: 16,
|
|
40
|
+
insetX: 32
|
|
41
|
+
},
|
|
42
|
+
/** FitLayout 의 sheet 구획 상단 라운드. */
|
|
43
|
+
fitSheetRadius: 16,
|
|
44
|
+
/** 오버레이 딤 불투명도. */
|
|
45
|
+
dimOpacity: .62
|
|
46
|
+
};
|
|
47
|
+
//#endregion
|
|
48
|
+
export { LAYOUT };
|
|
49
|
+
|
|
50
|
+
//# sourceMappingURL=tokens.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tokens.mjs","names":[],"sources":["../src/tokens.ts"],"sourcesContent":["/**\n * 레이아웃 수치. 사내 앱 6곳(openiot-203-webview · openiot-008-app · 202-app-client ·\n * 203-karking-guest-app · openiot-203-app · openiot-sample-app-react-native)에서\n * 반복 등장하던 값을 한 곳으로 모은 것이다.\n *\n * 웹과 RN 컴포넌트가 **같은 상수를 읽어** 두 플랫폼이 갈라지지 않게 한다.\n * 지금까지는 같은 숫자가 각 레포에 복사돼 있어서 조용히 어긋날 수 있었다.\n */\nexport const LAYOUT = {\n /** 모바일 콘텐츠 최대 폭. 데스크톱 브라우저에서 가운데 정렬 기준. */\n maxWidth: 428,\n\n /** 헤더. 높이는 최소값이며 내용에 따라 늘어난다(2단 헤더 등). */\n header: { minHeight: 40 },\n\n /** 본문 — px 20 / pt 24 / pb 32 / 섹션 간 gap 48. */\n content: { paddingX: 20, paddingTop: 24, paddingBottom: 32, gap: 48 },\n\n /** 하단 액션 영역 (CTA 버튼 한 줄). */\n footer: { height: 96, paddingX: 20, paddingY: 24 },\n\n /** 탭바 — 웹은 고정 60, RN 은 68 + safe-area. */\n tabBar: { web: 60, native: 68 },\n\n /** 섹션 묶음 간격 (구 ContentLayout). */\n stackGap: 16,\n\n /** 바텀시트 상단 라운드. */\n sheetRadius: 20,\n\n /** 다이얼로그 — 라운드와 좌우 여백. */\n dialog: { radius: 16, insetX: 32 },\n\n /** FitLayout 의 sheet 구획 상단 라운드. */\n fitSheetRadius: 16,\n\n /** 오버레이 딤 불투명도. */\n dimOpacity: 0.62,\n} as const\n\n/** 로고·브랜드 규칙과 무관한 순수 구조 수치라 색상은 포함하지 않는다. */\nexport type LayoutTokens = typeof LAYOUT\n"],"mappings":";;;;;;;;;AAQA,MAAa,SAAS;;CAEpB,UAAU;;CAGV,QAAQ,EAAE,WAAW,GAAG;;CAGxB,SAAS;EAAE,UAAU;EAAI,YAAY;EAAI,eAAe;EAAI,KAAK;CAAG;;CAGpE,QAAQ;EAAE,QAAQ;EAAI,UAAU;EAAI,UAAU;CAAG;;CAGjD,QAAQ;EAAE,KAAK;EAAI,QAAQ;CAAG;;CAG9B,UAAU;;CAGV,aAAa;;CAGb,QAAQ;EAAE,QAAQ;EAAI,QAAQ;CAAG;;CAGjC,gBAAgB;;CAGhB,YAAY;AACd"}
|
package/dist/types.d.mts
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
//#region src/types.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* 웹·RN 컴포넌트가 공유하는 prop 계약.
|
|
6
|
+
*
|
|
7
|
+
* 두 구현이 이 타입을 각각 확장하므로, 한쪽에만 prop 을 추가하면 타입은 통과하지만
|
|
8
|
+
* 계약이 갈라진다. 새 prop 은 반드시 여기에 먼저 추가한다.
|
|
9
|
+
*/
|
|
10
|
+
/** 화면 하단에 무엇이 오는가. */
|
|
11
|
+
interface ScreenLayoutBaseProps {
|
|
12
|
+
/** 상단 고정 영역. 높이는 최소 40px 이고 내용에 따라 늘어난다. */
|
|
13
|
+
header?: ReactNode;
|
|
14
|
+
/** 스크롤되는 본문. */
|
|
15
|
+
children: ReactNode;
|
|
16
|
+
/**
|
|
17
|
+
* 하단 고정 영역 — CTA 버튼이나 탭바.
|
|
18
|
+
* 탭바를 넣을 때는 `safeBottom={false}` 로 두어 탭바가 safe-area 를 직접 처리하게 한다.
|
|
19
|
+
*/
|
|
20
|
+
footer?: ReactNode;
|
|
21
|
+
/** footer 아래 배너 슬롯 (구 BasicLayout 의 bannerChildren). */
|
|
22
|
+
banner?: ReactNode;
|
|
23
|
+
/**
|
|
24
|
+
* 하단 safe-area 여백을 레이아웃이 넣을지 (기본 true).
|
|
25
|
+
* 탭바처럼 자체적으로 inset 을 처리하는 footer 를 쓸 때 false.
|
|
26
|
+
*/
|
|
27
|
+
safeBottom?: boolean;
|
|
28
|
+
/**
|
|
29
|
+
* 콘텐츠 최대 폭 (기본 428). `false` 면 폭 제한 없이 꽉 채운다.
|
|
30
|
+
* RN 에서는 항상 화면 폭이라 무시된다.
|
|
31
|
+
*/
|
|
32
|
+
maxWidth?: number | false;
|
|
33
|
+
}
|
|
34
|
+
/** FitLayout 의 구획 하나. */
|
|
35
|
+
interface FitRegion {
|
|
36
|
+
/** 고정 높이(px) 또는 남은 공간을 채우는 `'fill'`. */
|
|
37
|
+
height: number | 'fill';
|
|
38
|
+
/**
|
|
39
|
+
* `'sheet'` 면 상단 라운드 16 + 배경 전환이 붙어 기기제어형 시트가 된다.
|
|
40
|
+
* 보통 마지막 `'fill'` 구획에 준다.
|
|
41
|
+
*/
|
|
42
|
+
variant?: 'plain' | 'sheet';
|
|
43
|
+
/** 구획 내용. */
|
|
44
|
+
node: ReactNode;
|
|
45
|
+
/** React 리스트 key (미지정 시 인덱스). */
|
|
46
|
+
key?: string;
|
|
47
|
+
}
|
|
48
|
+
/** 스크롤 없이 한 화면에 배분하는 레이아웃. */
|
|
49
|
+
interface FitLayoutBaseProps {
|
|
50
|
+
header?: ReactNode;
|
|
51
|
+
/** 위에서 아래로 배치할 구획들. `'fill'` 은 하나만 두는 것을 권장한다. */
|
|
52
|
+
regions: FitRegion[];
|
|
53
|
+
/** 하단 액션 영역 (96px). */
|
|
54
|
+
action?: ReactNode;
|
|
55
|
+
/** sheet 구획 배경색. 지정하지 않으면 투명이라 부모 배경이 비친다. */
|
|
56
|
+
sheetBackground?: string;
|
|
57
|
+
safeBottom?: boolean;
|
|
58
|
+
maxWidth?: number | false;
|
|
59
|
+
}
|
|
60
|
+
/** 열림 상태를 밖에서 제어하는 오버레이 공통 prop. */
|
|
61
|
+
interface OverlayBaseProps {
|
|
62
|
+
open: boolean;
|
|
63
|
+
onOpenChange: (open: boolean) => void;
|
|
64
|
+
/** 배경(딤) 탭으로 닫히지 않게 하려면 false (기본 true). */
|
|
65
|
+
dismissible?: boolean;
|
|
66
|
+
}
|
|
67
|
+
interface BottomSheetBaseProps extends OverlayBaseProps {
|
|
68
|
+
children: ReactNode;
|
|
69
|
+
/** 스크린리더용 제목. 시각적 제목은 children 안에 직접 넣는다. */
|
|
70
|
+
label: string;
|
|
71
|
+
}
|
|
72
|
+
interface DialogBaseProps extends OverlayBaseProps {
|
|
73
|
+
title: string;
|
|
74
|
+
description?: ReactNode;
|
|
75
|
+
/** 왼쪽 보조 버튼. */
|
|
76
|
+
cancel?: {
|
|
77
|
+
label: string;
|
|
78
|
+
onPress?: () => void;
|
|
79
|
+
};
|
|
80
|
+
/** 오른쪽 주요 버튼. `tone: 'danger'` 면 파괴적 동작으로 표시된다. */
|
|
81
|
+
confirm?: {
|
|
82
|
+
label: string;
|
|
83
|
+
onPress?: () => void;
|
|
84
|
+
tone?: 'default' | 'danger';
|
|
85
|
+
};
|
|
86
|
+
/** 버튼 대신 직접 채우고 싶을 때. */
|
|
87
|
+
children?: ReactNode;
|
|
88
|
+
}
|
|
89
|
+
//#endregion
|
|
90
|
+
export { BottomSheetBaseProps, DialogBaseProps, FitLayoutBaseProps, FitRegion, OverlayBaseProps, ScreenLayoutBaseProps };
|
|
91
|
+
//# sourceMappingURL=types.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.mts","names":[],"sources":["../src/types.ts"],"mappings":";;;;;AAUA;;;;;UAAiB,qBAAA;EAWN;EATT,MAAA,GAAS,SAAA;EASS;EAPlB,QAAA,EAAU,SAAA;EAFD;;;;EAOT,MAAA,GAAS,SAAA;EAET;EAAA,MAAA,GAAS,SAAA;EAKT;;;AAKQ;EALR,UAAA;EASwB;;;;EAJxB,QAAA;AAAA;;UAIe,SAAA;EAWZ;EATH,MAAA;EAae;;;;EARf,OAAA;EAaS;EAXT,IAAA,EAAM,SAAS;EAWG;EATlB,GAAA;AAAA;;UAIe,kBAAA;EACf,MAAA,GAAS,SAAA;EAIA;EAFT,OAAA,EAAS,SAAA;EAKT;EAHA,MAAA,GAAS,SAAA;EAID;EAFR,eAAA;EACA,UAAA;EACA,QAAA;AAAA;;UAIe,gBAAA;EACf,IAAA;EACA,YAAA,GAAe,IAAA;EAEf;EAAA,WAAA;AAAA;AAAA,UAGe,oBAAA,SAA6B,gBAAgB;EAC5D,QAAA,EAAU,SAAA;;EAEV,KAAA;AAAA;AAAA,UAGe,eAAA,SAAwB,gBAAA;EACvC,KAAA;EACA,WAAA,GAAc,SAAA;EALT;EAOL,MAAA;IAAW,KAAA;IAAe,OAAA;EAAA;EAFZ;EAId,OAAA;IAAY,KAAA;IAAe,OAAA;IAAsB,IAAA;EAAA;EALjD;EAOA,QAAA,GAAW,SAAA;AAAA"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { FitLayoutBaseProps } from "../types.mjs";
|
|
2
|
+
import { CSSProperties } from "react";
|
|
3
|
+
|
|
4
|
+
//#region src/web/FitLayout.d.ts
|
|
5
|
+
interface FitLayoutProps extends FitLayoutBaseProps {
|
|
6
|
+
className?: string;
|
|
7
|
+
style?: CSSProperties;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* 고정형 화면 골격 — 스크롤 없이 구획을 배분한다.
|
|
11
|
+
*
|
|
12
|
+
* 구 `DeviceConnectLayout`(가이드 160 / 이미지 328 / 정보 fill)과
|
|
13
|
+
* `DeviceControlLayout`(서머리 336 / 시트 fill)을 대체한다. 둘의 차이는 구획 비율과
|
|
14
|
+
* 마지막 구획의 시트 여부뿐이라 `regions` 배열로 흡수된다.
|
|
15
|
+
*
|
|
16
|
+
* 단계가 바뀌어도 액션 버튼 위치가 흔들리지 않는 것이 이 레이아웃의 핵심이다.
|
|
17
|
+
* 내용 길이가 가변이면 `ScreenLayout` 을 쓴다.
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```tsx
|
|
21
|
+
* // 기기 연결 마법사
|
|
22
|
+
* <FitLayout
|
|
23
|
+
* header={<Header onBackPress={back} />}
|
|
24
|
+
* regions={[
|
|
25
|
+
* { height: 160, node: <Guide /> },
|
|
26
|
+
* { height: 328, node: <DeviceImage /> },
|
|
27
|
+
* { height: 'fill', node: <Info /> },
|
|
28
|
+
* ]}
|
|
29
|
+
* action={<Button>기기 연결하기</Button>}
|
|
30
|
+
* />
|
|
31
|
+
*
|
|
32
|
+
* // 기기 제어 — 마지막 구획을 시트로
|
|
33
|
+
* <FitLayout
|
|
34
|
+
* header={<Header title="거실 공기청정기" onBackPress={back} />}
|
|
35
|
+
* regions={[
|
|
36
|
+
* { height: 336, node: <Summary /> },
|
|
37
|
+
* { height: 'fill', variant: 'sheet', node: <Controls /> },
|
|
38
|
+
* ]}
|
|
39
|
+
* sheetBackground="#292E36"
|
|
40
|
+
* />
|
|
41
|
+
* ```
|
|
42
|
+
*/
|
|
43
|
+
declare function FitLayout({
|
|
44
|
+
header,
|
|
45
|
+
regions,
|
|
46
|
+
action,
|
|
47
|
+
sheetBackground,
|
|
48
|
+
safeBottom,
|
|
49
|
+
maxWidth,
|
|
50
|
+
className,
|
|
51
|
+
style
|
|
52
|
+
}: FitLayoutProps): import("react").JSX.Element;
|
|
53
|
+
//#endregion
|
|
54
|
+
export { FitLayout, FitLayoutProps };
|
|
55
|
+
//# sourceMappingURL=FitLayout.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FitLayout.d.mts","names":[],"sources":["../../src/web/FitLayout.tsx"],"mappings":";;;;UAIiB,cAAA,SAAuB,kBAAkB;EACxD,SAAA;EACA,KAAA,GAAQ,aAAA;AAAA;;;;;;;;AAAa;AAqDvB;;;;;;;;;;;;;;;;;;;;;;;;;;iBAAgB,SAAA;EACd,MAAA;EACA,OAAA;EACA,MAAA;EACA,eAAA;EACA,UAAA;EACA,QAAA;EACA,SAAA;EACA;AAAA,GACC,cAAA,mBAAc,GAAA,CAAA,OAAA"}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { LAYOUT } from "../tokens.mjs";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
//#region src/web/FitLayout.tsx
|
|
4
|
+
function regionStyle(region, sheetBackground) {
|
|
5
|
+
const base = region.height === "fill" ? {
|
|
6
|
+
flex: 1,
|
|
7
|
+
minHeight: 0
|
|
8
|
+
} : {
|
|
9
|
+
flex: "none",
|
|
10
|
+
height: region.height
|
|
11
|
+
};
|
|
12
|
+
if (region.variant !== "sheet") return {
|
|
13
|
+
...base,
|
|
14
|
+
width: "100%"
|
|
15
|
+
};
|
|
16
|
+
return {
|
|
17
|
+
...base,
|
|
18
|
+
width: "100%",
|
|
19
|
+
borderTopLeftRadius: LAYOUT.fitSheetRadius,
|
|
20
|
+
borderTopRightRadius: LAYOUT.fitSheetRadius,
|
|
21
|
+
overflow: "hidden",
|
|
22
|
+
...sheetBackground ? { backgroundColor: sheetBackground } : null
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* 고정형 화면 골격 — 스크롤 없이 구획을 배분한다.
|
|
27
|
+
*
|
|
28
|
+
* 구 `DeviceConnectLayout`(가이드 160 / 이미지 328 / 정보 fill)과
|
|
29
|
+
* `DeviceControlLayout`(서머리 336 / 시트 fill)을 대체한다. 둘의 차이는 구획 비율과
|
|
30
|
+
* 마지막 구획의 시트 여부뿐이라 `regions` 배열로 흡수된다.
|
|
31
|
+
*
|
|
32
|
+
* 단계가 바뀌어도 액션 버튼 위치가 흔들리지 않는 것이 이 레이아웃의 핵심이다.
|
|
33
|
+
* 내용 길이가 가변이면 `ScreenLayout` 을 쓴다.
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* ```tsx
|
|
37
|
+
* // 기기 연결 마법사
|
|
38
|
+
* <FitLayout
|
|
39
|
+
* header={<Header onBackPress={back} />}
|
|
40
|
+
* regions={[
|
|
41
|
+
* { height: 160, node: <Guide /> },
|
|
42
|
+
* { height: 328, node: <DeviceImage /> },
|
|
43
|
+
* { height: 'fill', node: <Info /> },
|
|
44
|
+
* ]}
|
|
45
|
+
* action={<Button>기기 연결하기</Button>}
|
|
46
|
+
* />
|
|
47
|
+
*
|
|
48
|
+
* // 기기 제어 — 마지막 구획을 시트로
|
|
49
|
+
* <FitLayout
|
|
50
|
+
* header={<Header title="거실 공기청정기" onBackPress={back} />}
|
|
51
|
+
* regions={[
|
|
52
|
+
* { height: 336, node: <Summary /> },
|
|
53
|
+
* { height: 'fill', variant: 'sheet', node: <Controls /> },
|
|
54
|
+
* ]}
|
|
55
|
+
* sheetBackground="#292E36"
|
|
56
|
+
* />
|
|
57
|
+
* ```
|
|
58
|
+
*/
|
|
59
|
+
function FitLayout({ header, regions, action, sheetBackground, safeBottom = true, maxWidth = LAYOUT.maxWidth, className, style }) {
|
|
60
|
+
return /* @__PURE__ */ jsx("div", {
|
|
61
|
+
style: {
|
|
62
|
+
display: "flex",
|
|
63
|
+
justifyContent: "center",
|
|
64
|
+
width: "100%"
|
|
65
|
+
},
|
|
66
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
67
|
+
className,
|
|
68
|
+
style: {
|
|
69
|
+
display: "flex",
|
|
70
|
+
flexDirection: "column",
|
|
71
|
+
width: "100%",
|
|
72
|
+
height: "100dvh",
|
|
73
|
+
overflow: "hidden",
|
|
74
|
+
...maxWidth === false ? null : { maxWidth },
|
|
75
|
+
...style
|
|
76
|
+
},
|
|
77
|
+
children: [
|
|
78
|
+
header !== void 0 && /* @__PURE__ */ jsx("header", {
|
|
79
|
+
style: {
|
|
80
|
+
flex: "none",
|
|
81
|
+
display: "flex",
|
|
82
|
+
flexDirection: "column",
|
|
83
|
+
justifyContent: "center",
|
|
84
|
+
width: "100%",
|
|
85
|
+
minHeight: LAYOUT.header.minHeight
|
|
86
|
+
},
|
|
87
|
+
children: header
|
|
88
|
+
}),
|
|
89
|
+
regions.map((region, i) => /* @__PURE__ */ jsx("div", {
|
|
90
|
+
style: regionStyle(region, sheetBackground),
|
|
91
|
+
children: region.node
|
|
92
|
+
}, region.key ?? i)),
|
|
93
|
+
action !== void 0 && /* @__PURE__ */ jsx("div", {
|
|
94
|
+
style: {
|
|
95
|
+
flex: "none",
|
|
96
|
+
width: "100%",
|
|
97
|
+
paddingLeft: LAYOUT.footer.paddingX,
|
|
98
|
+
paddingRight: LAYOUT.footer.paddingX,
|
|
99
|
+
paddingTop: LAYOUT.footer.paddingY,
|
|
100
|
+
paddingBottom: safeBottom ? `calc(${LAYOUT.footer.paddingY}px + env(safe-area-inset-bottom))` : LAYOUT.footer.paddingY
|
|
101
|
+
},
|
|
102
|
+
children: action
|
|
103
|
+
})
|
|
104
|
+
]
|
|
105
|
+
})
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
//#endregion
|
|
109
|
+
export { FitLayout };
|
|
110
|
+
|
|
111
|
+
//# sourceMappingURL=FitLayout.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FitLayout.mjs","names":[],"sources":["../../src/web/FitLayout.tsx"],"sourcesContent":["import type { CSSProperties } from 'react'\nimport { LAYOUT } from '../tokens.js'\nimport type { FitLayoutBaseProps, FitRegion } from '../types.js'\n\nexport interface FitLayoutProps extends FitLayoutBaseProps {\n className?: string\n style?: CSSProperties\n}\n\nfunction regionStyle(region: FitRegion, sheetBackground?: string): CSSProperties {\n const base: CSSProperties =\n region.height === 'fill' ? { flex: 1, minHeight: 0 } : { flex: 'none', height: region.height }\n\n if (region.variant !== 'sheet') return { ...base, width: '100%' }\n\n return {\n ...base,\n width: '100%',\n borderTopLeftRadius: LAYOUT.fitSheetRadius,\n borderTopRightRadius: LAYOUT.fitSheetRadius,\n overflow: 'hidden',\n ...(sheetBackground ? { backgroundColor: sheetBackground } : null),\n }\n}\n\n/**\n * 고정형 화면 골격 — 스크롤 없이 구획을 배분한다.\n *\n * 구 `DeviceConnectLayout`(가이드 160 / 이미지 328 / 정보 fill)과\n * `DeviceControlLayout`(서머리 336 / 시트 fill)을 대체한다. 둘의 차이는 구획 비율과\n * 마지막 구획의 시트 여부뿐이라 `regions` 배열로 흡수된다.\n *\n * 단계가 바뀌어도 액션 버튼 위치가 흔들리지 않는 것이 이 레이아웃의 핵심이다.\n * 내용 길이가 가변이면 `ScreenLayout` 을 쓴다.\n *\n * @example\n * ```tsx\n * // 기기 연결 마법사\n * <FitLayout\n * header={<Header onBackPress={back} />}\n * regions={[\n * { height: 160, node: <Guide /> },\n * { height: 328, node: <DeviceImage /> },\n * { height: 'fill', node: <Info /> },\n * ]}\n * action={<Button>기기 연결하기</Button>}\n * />\n *\n * // 기기 제어 — 마지막 구획을 시트로\n * <FitLayout\n * header={<Header title=\"거실 공기청정기\" onBackPress={back} />}\n * regions={[\n * { height: 336, node: <Summary /> },\n * { height: 'fill', variant: 'sheet', node: <Controls /> },\n * ]}\n * sheetBackground=\"#292E36\"\n * />\n * ```\n */\nexport function FitLayout({\n header,\n regions,\n action,\n sheetBackground,\n safeBottom = true,\n maxWidth = LAYOUT.maxWidth,\n className,\n style,\n}: FitLayoutProps) {\n const column: CSSProperties = {\n display: 'flex',\n flexDirection: 'column',\n width: '100%',\n height: '100dvh',\n overflow: 'hidden',\n ...(maxWidth === false ? null : { maxWidth }),\n }\n\n return (\n <div style={{ display: 'flex', justifyContent: 'center', width: '100%' }}>\n <div className={className} style={{ ...column, ...style }}>\n {header !== undefined && (\n <header\n style={{\n flex: 'none',\n display: 'flex',\n flexDirection: 'column',\n justifyContent: 'center',\n width: '100%',\n minHeight: LAYOUT.header.minHeight,\n }}\n >\n {header}\n </header>\n )}\n\n {regions.map((region, i) => (\n <div key={region.key ?? i} style={regionStyle(region, sheetBackground)}>\n {region.node}\n </div>\n ))}\n\n {action !== undefined && (\n <div\n style={{\n flex: 'none',\n width: '100%',\n paddingLeft: LAYOUT.footer.paddingX,\n paddingRight: LAYOUT.footer.paddingX,\n paddingTop: LAYOUT.footer.paddingY,\n paddingBottom: safeBottom\n ? `calc(${LAYOUT.footer.paddingY}px + env(safe-area-inset-bottom))`\n : LAYOUT.footer.paddingY,\n }}\n >\n {action}\n </div>\n )}\n </div>\n </div>\n )\n}\n"],"mappings":";;;AASA,SAAS,YAAY,QAAmB,iBAAyC;CAC/E,MAAM,OACJ,OAAO,WAAW,SAAS;EAAE,MAAM;EAAG,WAAW;CAAE,IAAI;EAAE,MAAM;EAAQ,QAAQ,OAAO;CAAO;CAE/F,IAAI,OAAO,YAAY,SAAS,OAAO;EAAE,GAAG;EAAM,OAAO;CAAO;CAEhE,OAAO;EACL,GAAG;EACH,OAAO;EACP,qBAAqB,OAAO;EAC5B,sBAAsB,OAAO;EAC7B,UAAU;EACV,GAAI,kBAAkB,EAAE,iBAAiB,gBAAgB,IAAI;CAC/D;AACF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoCA,SAAgB,UAAU,EACxB,QACA,SACA,QACA,iBACA,aAAa,MACb,WAAW,OAAO,UAClB,WACA,SACiB;CAUjB,OACE,oBAAC,OAAD;EAAK,OAAO;GAAE,SAAS;GAAQ,gBAAgB;GAAU,OAAO;EAAO;YACrE,qBAAC,OAAD;GAAgB;GAAW,OAAO;IAVpC,SAAS;IACT,eAAe;IACf,OAAO;IACP,QAAQ;IACR,UAAU;IACV,GAAI,aAAa,QAAQ,OAAO,EAAE,SAAS;IAKM,GAAG;GAAM;aAAxD;IACG,WAAW,KAAA,KACV,oBAAC,UAAD;KACE,OAAO;MACL,MAAM;MACN,SAAS;MACT,eAAe;MACf,gBAAgB;MAChB,OAAO;MACP,WAAW,OAAO,OAAO;KAC3B;eAEC;IACK,CAAA;IAGT,QAAQ,KAAK,QAAQ,MACpB,oBAAC,OAAD;KAA2B,OAAO,YAAY,QAAQ,eAAe;eAClE,OAAO;IACL,GAFK,OAAO,OAAO,CAEnB,CACN;IAEA,WAAW,KAAA,KACV,oBAAC,OAAD;KACE,OAAO;MACL,MAAM;MACN,OAAO;MACP,aAAa,OAAO,OAAO;MAC3B,cAAc,OAAO,OAAO;MAC5B,YAAY,OAAO,OAAO;MAC1B,eAAe,aACX,QAAQ,OAAO,OAAO,SAAS,qCAC/B,OAAO,OAAO;KACpB;eAEC;IACE,CAAA;GAEJ;;CACF,CAAA;AAET"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { ScreenLayoutBaseProps } from "../types.mjs";
|
|
2
|
+
import { CSSProperties, ReactNode } from "react";
|
|
3
|
+
|
|
4
|
+
//#region src/web/ScreenLayout.d.ts
|
|
5
|
+
interface ScreenLayoutProps extends ScreenLayoutBaseProps {
|
|
6
|
+
className?: string;
|
|
7
|
+
style?: CSSProperties;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* 스크롤형 화면 골격 — 헤더 고정 + 본문 스크롤 + 하단 슬롯.
|
|
11
|
+
*
|
|
12
|
+
* 구 `BasicLayout` · `TabsLayout` · `ContentLayout` 의 하단 CTA · smart-key 의 full-bleed
|
|
13
|
+
* 조립을 모두 대체한다. 하단에 무엇이 오느냐(`footer`)만 달랐던 것들이라 prop 하나로 흡수된다.
|
|
14
|
+
*
|
|
15
|
+
* 하단 CTA 를 그릴 수 있는 곳은 여기 `footer` 하나뿐이다. 예전에는 BasicLayout 과
|
|
16
|
+
* ContentLayout 양쪽에서 그릴 수 있어 같은 화면에 CTA 가 두 번 잡히는 일이 있었다.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```tsx
|
|
20
|
+
* <ScreenLayout header={<Header title="방문차량 등록" onBackPress={back} />}
|
|
21
|
+
* footer={<Button>등록하기</Button>}>
|
|
22
|
+
* <Stack>…</Stack>
|
|
23
|
+
* </ScreenLayout>
|
|
24
|
+
*
|
|
25
|
+
* // 탭 하위 화면 — 탭바가 safe-area 를 직접 처리한다
|
|
26
|
+
* <ScreenLayout header={<Header leftIcon={<Logo />} />}
|
|
27
|
+
* footer={<TabBar />} safeBottom={false}>
|
|
28
|
+
* …
|
|
29
|
+
* </ScreenLayout>
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
declare function ScreenLayout({
|
|
33
|
+
header,
|
|
34
|
+
children,
|
|
35
|
+
footer,
|
|
36
|
+
banner,
|
|
37
|
+
safeBottom,
|
|
38
|
+
maxWidth,
|
|
39
|
+
className,
|
|
40
|
+
style
|
|
41
|
+
}: ScreenLayoutProps): import("react").JSX.Element;
|
|
42
|
+
interface StackProps {
|
|
43
|
+
children: ReactNode;
|
|
44
|
+
/** 항목 간격 (기본 16). */
|
|
45
|
+
gap?: number;
|
|
46
|
+
className?: string;
|
|
47
|
+
style?: CSSProperties;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* 섹션을 일정 간격으로 쌓는 유틸. 구 `ContentLayout` 의 실체이며,
|
|
51
|
+
* 레이아웃이 아니라 간격 유틸이라 하단 CTA 슬롯은 갖지 않는다 — 그건 `ScreenLayout.footer` 다.
|
|
52
|
+
*/
|
|
53
|
+
declare function Stack({
|
|
54
|
+
children,
|
|
55
|
+
gap,
|
|
56
|
+
className,
|
|
57
|
+
style
|
|
58
|
+
}: StackProps): import("react").JSX.Element;
|
|
59
|
+
//#endregion
|
|
60
|
+
export { ScreenLayout, ScreenLayoutProps, Stack, StackProps };
|
|
61
|
+
//# sourceMappingURL=ScreenLayout.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ScreenLayout.d.mts","names":[],"sources":["../../src/web/ScreenLayout.tsx"],"mappings":";;;;UAIiB,iBAAA,SAA0B,qBAAqB;EAC9D,SAAA;EACA,KAAA,GAAQ,aAAA;AAAA;;;;;;;;AAAa;AA0BvB;;;;;;;;;;;;;;;iBAAgB,YAAA;EACd,MAAA;EACA,QAAA;EACA,MAAA;EACA,MAAA;EACA,UAAA;EACA,QAAA;EACA,SAAA;EACA;AAAA,GACC,iBAAA,mBAAiB,GAAA,CAAA,OAAA;AAAA,UA6EH,UAAA;EACf,QAAA,EAAU,SAAA;EApFV;EAsFA,GAAA;EACA,SAAA;EACA,KAAA,GAAQ,aAAa;AAAA;;;;;iBAOP,KAAA;EAAQ,QAAA;EAAU,GAAA;EAAuB,SAAA;EAAW;AAAA,GAAS,UAAA,mBAAU,GAAA,CAAA,OAAA"}
|