@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,118 @@
|
|
|
1
|
+
import { LAYOUT } from "../tokens.mjs";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
//#region src/web/ScreenLayout.tsx
|
|
4
|
+
/**
|
|
5
|
+
* 스크롤형 화면 골격 — 헤더 고정 + 본문 스크롤 + 하단 슬롯.
|
|
6
|
+
*
|
|
7
|
+
* 구 `BasicLayout` · `TabsLayout` · `ContentLayout` 의 하단 CTA · smart-key 의 full-bleed
|
|
8
|
+
* 조립을 모두 대체한다. 하단에 무엇이 오느냐(`footer`)만 달랐던 것들이라 prop 하나로 흡수된다.
|
|
9
|
+
*
|
|
10
|
+
* 하단 CTA 를 그릴 수 있는 곳은 여기 `footer` 하나뿐이다. 예전에는 BasicLayout 과
|
|
11
|
+
* ContentLayout 양쪽에서 그릴 수 있어 같은 화면에 CTA 가 두 번 잡히는 일이 있었다.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```tsx
|
|
15
|
+
* <ScreenLayout header={<Header title="방문차량 등록" onBackPress={back} />}
|
|
16
|
+
* footer={<Button>등록하기</Button>}>
|
|
17
|
+
* <Stack>…</Stack>
|
|
18
|
+
* </ScreenLayout>
|
|
19
|
+
*
|
|
20
|
+
* // 탭 하위 화면 — 탭바가 safe-area 를 직접 처리한다
|
|
21
|
+
* <ScreenLayout header={<Header leftIcon={<Logo />} />}
|
|
22
|
+
* footer={<TabBar />} safeBottom={false}>
|
|
23
|
+
* …
|
|
24
|
+
* </ScreenLayout>
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
function ScreenLayout({ header, children, footer, banner, safeBottom = true, maxWidth = LAYOUT.maxWidth, className, style }) {
|
|
28
|
+
return /* @__PURE__ */ jsx("div", {
|
|
29
|
+
style: {
|
|
30
|
+
display: "flex",
|
|
31
|
+
justifyContent: "center",
|
|
32
|
+
width: "100%"
|
|
33
|
+
},
|
|
34
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
35
|
+
className,
|
|
36
|
+
style: {
|
|
37
|
+
display: "flex",
|
|
38
|
+
flexDirection: "column",
|
|
39
|
+
width: "100%",
|
|
40
|
+
height: "100dvh",
|
|
41
|
+
overflow: "hidden",
|
|
42
|
+
...maxWidth === false ? null : { maxWidth },
|
|
43
|
+
...style
|
|
44
|
+
},
|
|
45
|
+
children: [
|
|
46
|
+
header !== void 0 && /* @__PURE__ */ jsx("header", {
|
|
47
|
+
style: {
|
|
48
|
+
position: "sticky",
|
|
49
|
+
top: 0,
|
|
50
|
+
zIndex: 50,
|
|
51
|
+
flex: "none",
|
|
52
|
+
display: "flex",
|
|
53
|
+
flexDirection: "column",
|
|
54
|
+
justifyContent: "center",
|
|
55
|
+
width: "100%",
|
|
56
|
+
minHeight: LAYOUT.header.minHeight
|
|
57
|
+
},
|
|
58
|
+
children: header
|
|
59
|
+
}),
|
|
60
|
+
/* @__PURE__ */ jsx("div", {
|
|
61
|
+
style: {
|
|
62
|
+
flex: 1,
|
|
63
|
+
display: "flex",
|
|
64
|
+
flexDirection: "column",
|
|
65
|
+
width: "100%",
|
|
66
|
+
minHeight: 0,
|
|
67
|
+
overflowY: "auto",
|
|
68
|
+
overflowX: "hidden",
|
|
69
|
+
gap: LAYOUT.content.gap,
|
|
70
|
+
paddingLeft: LAYOUT.content.paddingX,
|
|
71
|
+
paddingRight: LAYOUT.content.paddingX,
|
|
72
|
+
paddingTop: LAYOUT.content.paddingTop,
|
|
73
|
+
paddingBottom: footer === void 0 && safeBottom ? `calc(${LAYOUT.content.paddingBottom}px + env(safe-area-inset-bottom))` : LAYOUT.content.paddingBottom
|
|
74
|
+
},
|
|
75
|
+
children
|
|
76
|
+
}),
|
|
77
|
+
footer !== void 0 && /* @__PURE__ */ jsx("div", {
|
|
78
|
+
style: {
|
|
79
|
+
flex: "none",
|
|
80
|
+
width: "100%",
|
|
81
|
+
paddingLeft: LAYOUT.footer.paddingX,
|
|
82
|
+
paddingRight: LAYOUT.footer.paddingX,
|
|
83
|
+
paddingTop: LAYOUT.footer.paddingY,
|
|
84
|
+
paddingBottom: safeBottom ? `calc(${LAYOUT.footer.paddingY}px + env(safe-area-inset-bottom))` : LAYOUT.footer.paddingY
|
|
85
|
+
},
|
|
86
|
+
children: footer
|
|
87
|
+
}),
|
|
88
|
+
banner !== void 0 && /* @__PURE__ */ jsx("div", {
|
|
89
|
+
style: {
|
|
90
|
+
flex: "none",
|
|
91
|
+
width: "100%"
|
|
92
|
+
},
|
|
93
|
+
children: banner
|
|
94
|
+
})
|
|
95
|
+
]
|
|
96
|
+
})
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* 섹션을 일정 간격으로 쌓는 유틸. 구 `ContentLayout` 의 실체이며,
|
|
101
|
+
* 레이아웃이 아니라 간격 유틸이라 하단 CTA 슬롯은 갖지 않는다 — 그건 `ScreenLayout.footer` 다.
|
|
102
|
+
*/
|
|
103
|
+
function Stack({ children, gap = LAYOUT.stackGap, className, style }) {
|
|
104
|
+
return /* @__PURE__ */ jsx("div", {
|
|
105
|
+
className,
|
|
106
|
+
style: {
|
|
107
|
+
display: "flex",
|
|
108
|
+
flexDirection: "column",
|
|
109
|
+
gap,
|
|
110
|
+
...style
|
|
111
|
+
},
|
|
112
|
+
children
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
//#endregion
|
|
116
|
+
export { ScreenLayout, Stack };
|
|
117
|
+
|
|
118
|
+
//# sourceMappingURL=ScreenLayout.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ScreenLayout.mjs","names":[],"sources":["../../src/web/ScreenLayout.tsx"],"sourcesContent":["import type { CSSProperties, ReactNode } from 'react'\nimport { LAYOUT } from '../tokens.js'\nimport type { ScreenLayoutBaseProps } from '../types.js'\n\nexport interface ScreenLayoutProps extends ScreenLayoutBaseProps {\n className?: string\n style?: CSSProperties\n}\n\n/**\n * 스크롤형 화면 골격 — 헤더 고정 + 본문 스크롤 + 하단 슬롯.\n *\n * 구 `BasicLayout` · `TabsLayout` · `ContentLayout` 의 하단 CTA · smart-key 의 full-bleed\n * 조립을 모두 대체한다. 하단에 무엇이 오느냐(`footer`)만 달랐던 것들이라 prop 하나로 흡수된다.\n *\n * 하단 CTA 를 그릴 수 있는 곳은 여기 `footer` 하나뿐이다. 예전에는 BasicLayout 과\n * ContentLayout 양쪽에서 그릴 수 있어 같은 화면에 CTA 가 두 번 잡히는 일이 있었다.\n *\n * @example\n * ```tsx\n * <ScreenLayout header={<Header title=\"방문차량 등록\" onBackPress={back} />}\n * footer={<Button>등록하기</Button>}>\n * <Stack>…</Stack>\n * </ScreenLayout>\n *\n * // 탭 하위 화면 — 탭바가 safe-area 를 직접 처리한다\n * <ScreenLayout header={<Header leftIcon={<Logo />} />}\n * footer={<TabBar />} safeBottom={false}>\n * …\n * </ScreenLayout>\n * ```\n */\nexport function ScreenLayout({\n header,\n children,\n footer,\n banner,\n safeBottom = true,\n maxWidth = LAYOUT.maxWidth,\n className,\n style,\n}: ScreenLayoutProps) {\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 position: 'sticky',\n top: 0,\n zIndex: 50,\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 <div\n style={{\n flex: 1,\n display: 'flex',\n flexDirection: 'column',\n width: '100%',\n minHeight: 0,\n overflowY: 'auto',\n overflowX: 'hidden',\n gap: LAYOUT.content.gap,\n paddingLeft: LAYOUT.content.paddingX,\n paddingRight: LAYOUT.content.paddingX,\n paddingTop: LAYOUT.content.paddingTop,\n // footer 가 없을 때만 본문이 하단 safe-area 를 책임진다.\n paddingBottom:\n footer === undefined && safeBottom\n ? `calc(${LAYOUT.content.paddingBottom}px + env(safe-area-inset-bottom))`\n : LAYOUT.content.paddingBottom,\n }}\n >\n {children}\n </div>\n\n {footer !== 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 {footer}\n </div>\n )}\n\n {banner !== undefined && <div style={{ flex: 'none', width: '100%' }}>{banner}</div>}\n </div>\n </div>\n )\n}\n\nexport interface StackProps {\n children: ReactNode\n /** 항목 간격 (기본 16). */\n gap?: number\n className?: string\n style?: CSSProperties\n}\n\n/**\n * 섹션을 일정 간격으로 쌓는 유틸. 구 `ContentLayout` 의 실체이며,\n * 레이아웃이 아니라 간격 유틸이라 하단 CTA 슬롯은 갖지 않는다 — 그건 `ScreenLayout.footer` 다.\n */\nexport function Stack({ children, gap = LAYOUT.stackGap, className, style }: StackProps) {\n return (\n <div className={className} style={{ display: 'flex', flexDirection: 'column', gap, ...style }}>\n {children}\n </div>\n )\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;AAgCA,SAAgB,aAAa,EAC3B,QACA,UACA,QACA,QACA,aAAa,MACb,WAAW,OAAO,UAClB,WACA,SACoB;CAUpB,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,UAAU;MACV,KAAK;MACL,QAAQ;MACR,MAAM;MACN,SAAS;MACT,eAAe;MACf,gBAAgB;MAChB,OAAO;MACP,WAAW,OAAO,OAAO;KAC3B;eAEC;IACK,CAAA;IAGV,oBAAC,OAAD;KACE,OAAO;MACL,MAAM;MACN,SAAS;MACT,eAAe;MACf,OAAO;MACP,WAAW;MACX,WAAW;MACX,WAAW;MACX,KAAK,OAAO,QAAQ;MACpB,aAAa,OAAO,QAAQ;MAC5B,cAAc,OAAO,QAAQ;MAC7B,YAAY,OAAO,QAAQ;MAE3B,eACE,WAAW,KAAA,KAAa,aACpB,QAAQ,OAAO,QAAQ,cAAc,qCACrC,OAAO,QAAQ;KACvB;KAEC;IACE,CAAA;IAEJ,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;IAGN,WAAW,KAAA,KAAa,oBAAC,OAAD;KAAK,OAAO;MAAE,MAAM;MAAQ,OAAO;KAAO;eAAI;IAAY,CAAA;GAChF;;CACF,CAAA;AAET;;;;;AAcA,SAAgB,MAAM,EAAE,UAAU,MAAM,OAAO,UAAU,WAAW,SAAqB;CACvF,OACE,oBAAC,OAAD;EAAgB;EAAW,OAAO;GAAE,SAAS;GAAQ,eAAe;GAAU;GAAK,GAAG;EAAM;EACzF;CACE,CAAA;AAET"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { BottomSheetBaseProps, DialogBaseProps } from "../types.mjs";
|
|
2
|
+
import { CSSProperties } from "react";
|
|
3
|
+
|
|
4
|
+
//#region src/web/overlays.d.ts
|
|
5
|
+
interface BottomSheetProps extends BottomSheetBaseProps {
|
|
6
|
+
/** 시트 폭 상한 (기본 428). */
|
|
7
|
+
maxWidth?: number | false;
|
|
8
|
+
className?: string;
|
|
9
|
+
style?: CSSProperties;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* 아래에서 올라오는 시트.
|
|
13
|
+
*
|
|
14
|
+
* 구 `BottomSheetCalendar` · `BottomSheetTimePicker` · `BottomSheetBanner` 를 대체한다.
|
|
15
|
+
* 세 변형의 차이는 안에 들어가는 내용뿐이라 껍데기만 공통화했다.
|
|
16
|
+
*
|
|
17
|
+
* shadcn `sheet` 와 달리 `side` 를 열지 않는다 — 실사용이 bottom 하나뿐인데 4방향을
|
|
18
|
+
* 열어두면 쓰이지 않을 조합까지 유지보수해야 한다.
|
|
19
|
+
*
|
|
20
|
+
* 열릴 때 {@link setDimHandler} 로 등록한 딤 핸들러를 자동으로 호출한다.
|
|
21
|
+
*/
|
|
22
|
+
declare function BottomSheet({
|
|
23
|
+
open,
|
|
24
|
+
onOpenChange,
|
|
25
|
+
dismissible,
|
|
26
|
+
label,
|
|
27
|
+
children,
|
|
28
|
+
maxWidth,
|
|
29
|
+
className,
|
|
30
|
+
style
|
|
31
|
+
}: BottomSheetProps): import("react").JSX.Element;
|
|
32
|
+
interface DialogProps extends DialogBaseProps {
|
|
33
|
+
maxWidth?: number | false;
|
|
34
|
+
className?: string;
|
|
35
|
+
style?: CSSProperties;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* 확인이 필요한 동작에 쓰는 중앙 다이얼로그.
|
|
39
|
+
*
|
|
40
|
+
* 구 `StandardModal`(RN)과 `AppDialog`(웹)를 하나의 계약으로 합쳤다.
|
|
41
|
+
* 버튼을 직접 구성하고 싶으면 `children` 을 쓰고, 그러면 `cancel`/`confirm` 은 무시된다.
|
|
42
|
+
*/
|
|
43
|
+
declare function Dialog({
|
|
44
|
+
open,
|
|
45
|
+
onOpenChange,
|
|
46
|
+
dismissible,
|
|
47
|
+
title,
|
|
48
|
+
description,
|
|
49
|
+
cancel,
|
|
50
|
+
confirm,
|
|
51
|
+
children,
|
|
52
|
+
maxWidth,
|
|
53
|
+
className,
|
|
54
|
+
style
|
|
55
|
+
}: DialogProps): import("react").JSX.Element;
|
|
56
|
+
//#endregion
|
|
57
|
+
export { BottomSheet, BottomSheetProps, Dialog, DialogProps };
|
|
58
|
+
//# sourceMappingURL=overlays.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"overlays.d.mts","names":[],"sources":["../../src/web/overlays.tsx"],"mappings":";;;;UAqCiB,gBAAA,SAAyB,oBAAoB;;EAE5D,QAAA;EACA,SAAA;EACA,KAAA,GAAQ,aAAA;AAAA;;;;;;;AAAa;AAcvB;;;;iBAAgB,WAAA;EACd,IAAA;EACA,YAAA;EACA,WAAA;EACA,KAAA;EACA,QAAA;EACA,QAAA;EACA,SAAA;EACA;AAAA,GACC,gBAAA,mBAAgB,GAAA,CAAA,OAAA;AAAA,UAoDF,WAAA,SAAoB,eAAe;EAClD,QAAA;EACA,SAAA;EACA,KAAA,GAAQ,aAAA;AAAA;;;;;;;iBASM,MAAA;EACd,IAAA;EACA,YAAA;EACA,WAAA;EACA,KAAA;EACA,WAAA;EACA,MAAA;EACA,OAAA;EACA,QAAA;EACA,QAAA;EACA,SAAA;EACA;AAAA,GACC,WAAA,mBAAW,GAAA,CAAA,OAAA"}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { LAYOUT } from "../tokens.mjs";
|
|
3
|
+
import { acquireDim, releaseDim } from "../dim-bridge.mjs";
|
|
4
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
5
|
+
import * as RadixDialog from "@radix-ui/react-dialog";
|
|
6
|
+
import { useEffect } from "react";
|
|
7
|
+
//#region src/web/overlays.tsx
|
|
8
|
+
/** 오버레이가 열려 있는 동안 네이티브 딤을 잡아 둔다 (웹뷰가 아니면 no-op). */
|
|
9
|
+
function useDim(open) {
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
if (!open) return;
|
|
12
|
+
acquireDim();
|
|
13
|
+
return releaseDim;
|
|
14
|
+
}, [open]);
|
|
15
|
+
}
|
|
16
|
+
const overlayStyle = {
|
|
17
|
+
position: "fixed",
|
|
18
|
+
inset: 0,
|
|
19
|
+
zIndex: 100,
|
|
20
|
+
backgroundColor: `rgba(0, 0, 0, ${LAYOUT.dimOpacity})`
|
|
21
|
+
};
|
|
22
|
+
/** 시트·다이얼로그를 모바일 폭에 맞춰 가운데 정렬하기 위한 래퍼. */
|
|
23
|
+
function centered(maxWidth) {
|
|
24
|
+
return {
|
|
25
|
+
position: "fixed",
|
|
26
|
+
left: 0,
|
|
27
|
+
right: 0,
|
|
28
|
+
zIndex: 101,
|
|
29
|
+
display: "flex",
|
|
30
|
+
justifyContent: "center",
|
|
31
|
+
...maxWidth === false ? null : { pointerEvents: "none" }
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* 아래에서 올라오는 시트.
|
|
36
|
+
*
|
|
37
|
+
* 구 `BottomSheetCalendar` · `BottomSheetTimePicker` · `BottomSheetBanner` 를 대체한다.
|
|
38
|
+
* 세 변형의 차이는 안에 들어가는 내용뿐이라 껍데기만 공통화했다.
|
|
39
|
+
*
|
|
40
|
+
* shadcn `sheet` 와 달리 `side` 를 열지 않는다 — 실사용이 bottom 하나뿐인데 4방향을
|
|
41
|
+
* 열어두면 쓰이지 않을 조합까지 유지보수해야 한다.
|
|
42
|
+
*
|
|
43
|
+
* 열릴 때 {@link setDimHandler} 로 등록한 딤 핸들러를 자동으로 호출한다.
|
|
44
|
+
*/
|
|
45
|
+
function BottomSheet({ open, onOpenChange, dismissible = true, label, children, maxWidth = LAYOUT.maxWidth, className, style }) {
|
|
46
|
+
useDim(open);
|
|
47
|
+
return /* @__PURE__ */ jsx(RadixDialog.Root, {
|
|
48
|
+
open,
|
|
49
|
+
onOpenChange,
|
|
50
|
+
children: /* @__PURE__ */ jsxs(RadixDialog.Portal, { children: [/* @__PURE__ */ jsx(RadixDialog.Overlay, { style: overlayStyle }), /* @__PURE__ */ jsx("div", {
|
|
51
|
+
style: {
|
|
52
|
+
...centered(maxWidth),
|
|
53
|
+
bottom: 0
|
|
54
|
+
},
|
|
55
|
+
children: /* @__PURE__ */ jsxs(RadixDialog.Content, {
|
|
56
|
+
className,
|
|
57
|
+
"aria-label": label,
|
|
58
|
+
onPointerDownOutside: (e) => {
|
|
59
|
+
if (!dismissible) e.preventDefault();
|
|
60
|
+
},
|
|
61
|
+
onEscapeKeyDown: (e) => {
|
|
62
|
+
if (!dismissible) e.preventDefault();
|
|
63
|
+
},
|
|
64
|
+
style: {
|
|
65
|
+
pointerEvents: "auto",
|
|
66
|
+
width: "100%",
|
|
67
|
+
...maxWidth === false ? null : { maxWidth },
|
|
68
|
+
borderTopLeftRadius: LAYOUT.sheetRadius,
|
|
69
|
+
borderTopRightRadius: LAYOUT.sheetRadius,
|
|
70
|
+
paddingLeft: LAYOUT.content.paddingX,
|
|
71
|
+
paddingRight: LAYOUT.content.paddingX,
|
|
72
|
+
paddingTop: 12,
|
|
73
|
+
paddingBottom: `calc(24px + env(safe-area-inset-bottom))`,
|
|
74
|
+
maxHeight: "86dvh",
|
|
75
|
+
overflowY: "auto",
|
|
76
|
+
...style
|
|
77
|
+
},
|
|
78
|
+
children: [/* @__PURE__ */ jsx(RadixDialog.Title, {
|
|
79
|
+
style: {
|
|
80
|
+
position: "absolute",
|
|
81
|
+
width: 1,
|
|
82
|
+
height: 1,
|
|
83
|
+
overflow: "hidden",
|
|
84
|
+
clip: "rect(0 0 0 0)",
|
|
85
|
+
whiteSpace: "nowrap"
|
|
86
|
+
},
|
|
87
|
+
children: label
|
|
88
|
+
}), children]
|
|
89
|
+
})
|
|
90
|
+
})] })
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* 확인이 필요한 동작에 쓰는 중앙 다이얼로그.
|
|
95
|
+
*
|
|
96
|
+
* 구 `StandardModal`(RN)과 `AppDialog`(웹)를 하나의 계약으로 합쳤다.
|
|
97
|
+
* 버튼을 직접 구성하고 싶으면 `children` 을 쓰고, 그러면 `cancel`/`confirm` 은 무시된다.
|
|
98
|
+
*/
|
|
99
|
+
function Dialog({ open, onOpenChange, dismissible = true, title, description, cancel, confirm, children, maxWidth = LAYOUT.maxWidth - LAYOUT.dialog.insetX * 2, className, style }) {
|
|
100
|
+
useDim(open);
|
|
101
|
+
return /* @__PURE__ */ jsx(RadixDialog.Root, {
|
|
102
|
+
open,
|
|
103
|
+
onOpenChange,
|
|
104
|
+
children: /* @__PURE__ */ jsxs(RadixDialog.Portal, { children: [/* @__PURE__ */ jsx(RadixDialog.Overlay, { style: overlayStyle }), /* @__PURE__ */ jsx("div", {
|
|
105
|
+
style: {
|
|
106
|
+
...centered(maxWidth),
|
|
107
|
+
top: 0,
|
|
108
|
+
bottom: 0,
|
|
109
|
+
alignItems: "center"
|
|
110
|
+
},
|
|
111
|
+
children: /* @__PURE__ */ jsxs(RadixDialog.Content, {
|
|
112
|
+
className,
|
|
113
|
+
onPointerDownOutside: (e) => {
|
|
114
|
+
if (!dismissible) e.preventDefault();
|
|
115
|
+
},
|
|
116
|
+
onEscapeKeyDown: (e) => {
|
|
117
|
+
if (!dismissible) e.preventDefault();
|
|
118
|
+
},
|
|
119
|
+
style: {
|
|
120
|
+
pointerEvents: "auto",
|
|
121
|
+
width: `calc(100% - ${LAYOUT.dialog.insetX * 2}px)`,
|
|
122
|
+
...maxWidth === false ? null : { maxWidth },
|
|
123
|
+
borderRadius: LAYOUT.dialog.radius,
|
|
124
|
+
padding: "26px 22px 18px",
|
|
125
|
+
display: "flex",
|
|
126
|
+
flexDirection: "column",
|
|
127
|
+
gap: 12,
|
|
128
|
+
textAlign: "center",
|
|
129
|
+
...style
|
|
130
|
+
},
|
|
131
|
+
children: [
|
|
132
|
+
/* @__PURE__ */ jsx(RadixDialog.Title, {
|
|
133
|
+
style: { margin: 0 },
|
|
134
|
+
children: title
|
|
135
|
+
}),
|
|
136
|
+
description !== void 0 && /* @__PURE__ */ jsx(RadixDialog.Description, {
|
|
137
|
+
style: { margin: 0 },
|
|
138
|
+
children: description
|
|
139
|
+
}),
|
|
140
|
+
children ?? /* @__PURE__ */ jsxs("div", {
|
|
141
|
+
style: {
|
|
142
|
+
display: "flex",
|
|
143
|
+
gap: 10,
|
|
144
|
+
marginTop: 6
|
|
145
|
+
},
|
|
146
|
+
children: [cancel && /* @__PURE__ */ jsx(RadixDialog.Close, {
|
|
147
|
+
asChild: true,
|
|
148
|
+
children: /* @__PURE__ */ jsx("button", {
|
|
149
|
+
type: "button",
|
|
150
|
+
style: { flex: 1 },
|
|
151
|
+
onClick: cancel.onPress,
|
|
152
|
+
children: cancel.label
|
|
153
|
+
})
|
|
154
|
+
}), confirm && /* @__PURE__ */ jsx("button", {
|
|
155
|
+
type: "button",
|
|
156
|
+
style: { flex: 1 },
|
|
157
|
+
"data-tone": confirm.tone ?? "default",
|
|
158
|
+
onClick: () => {
|
|
159
|
+
confirm.onPress?.();
|
|
160
|
+
onOpenChange(false);
|
|
161
|
+
},
|
|
162
|
+
children: confirm.label
|
|
163
|
+
})]
|
|
164
|
+
})
|
|
165
|
+
]
|
|
166
|
+
})
|
|
167
|
+
})] })
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
//#endregion
|
|
171
|
+
export { BottomSheet, Dialog };
|
|
172
|
+
|
|
173
|
+
//# sourceMappingURL=overlays.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"overlays.mjs","names":[],"sources":["../../src/web/overlays.tsx"],"sourcesContent":["'use client'\n\nimport * as RadixDialog from '@radix-ui/react-dialog'\nimport { useEffect, type CSSProperties } from 'react'\nimport { acquireDim, releaseDim } from '../dim-bridge.js'\nimport { LAYOUT } from '../tokens.js'\nimport type { BottomSheetBaseProps, DialogBaseProps } from '../types.js'\n\n/** 오버레이가 열려 있는 동안 네이티브 딤을 잡아 둔다 (웹뷰가 아니면 no-op). */\nfunction useDim(open: boolean) {\n useEffect(() => {\n if (!open) return\n acquireDim()\n return releaseDim\n }, [open])\n}\n\nconst overlayStyle: CSSProperties = {\n position: 'fixed',\n inset: 0,\n zIndex: 100,\n backgroundColor: `rgba(0, 0, 0, ${LAYOUT.dimOpacity})`,\n}\n\n/** 시트·다이얼로그를 모바일 폭에 맞춰 가운데 정렬하기 위한 래퍼. */\nfunction centered(maxWidth: number | false): CSSProperties {\n return {\n position: 'fixed',\n left: 0,\n right: 0,\n zIndex: 101,\n display: 'flex',\n justifyContent: 'center',\n ...(maxWidth === false ? null : { pointerEvents: 'none' as const }),\n }\n}\n\nexport interface BottomSheetProps extends BottomSheetBaseProps {\n /** 시트 폭 상한 (기본 428). */\n maxWidth?: number | false\n className?: string\n style?: CSSProperties\n}\n\n/**\n * 아래에서 올라오는 시트.\n *\n * 구 `BottomSheetCalendar` · `BottomSheetTimePicker` · `BottomSheetBanner` 를 대체한다.\n * 세 변형의 차이는 안에 들어가는 내용뿐이라 껍데기만 공통화했다.\n *\n * shadcn `sheet` 와 달리 `side` 를 열지 않는다 — 실사용이 bottom 하나뿐인데 4방향을\n * 열어두면 쓰이지 않을 조합까지 유지보수해야 한다.\n *\n * 열릴 때 {@link setDimHandler} 로 등록한 딤 핸들러를 자동으로 호출한다.\n */\nexport function BottomSheet({\n open,\n onOpenChange,\n dismissible = true,\n label,\n children,\n maxWidth = LAYOUT.maxWidth,\n className,\n style,\n}: BottomSheetProps) {\n useDim(open)\n\n return (\n <RadixDialog.Root open={open} onOpenChange={onOpenChange}>\n <RadixDialog.Portal>\n <RadixDialog.Overlay style={overlayStyle} />\n <div style={{ ...centered(maxWidth), bottom: 0 }}>\n <RadixDialog.Content\n className={className}\n aria-label={label}\n onPointerDownOutside={(e) => {\n if (!dismissible) e.preventDefault()\n }}\n onEscapeKeyDown={(e) => {\n if (!dismissible) e.preventDefault()\n }}\n style={{\n pointerEvents: 'auto',\n width: '100%',\n ...(maxWidth === false ? null : { maxWidth }),\n borderTopLeftRadius: LAYOUT.sheetRadius,\n borderTopRightRadius: LAYOUT.sheetRadius,\n paddingLeft: LAYOUT.content.paddingX,\n paddingRight: LAYOUT.content.paddingX,\n paddingTop: 12,\n paddingBottom: `calc(24px + env(safe-area-inset-bottom))`,\n maxHeight: '86dvh',\n overflowY: 'auto',\n ...style,\n }}\n >\n <RadixDialog.Title\n style={{\n position: 'absolute',\n width: 1,\n height: 1,\n overflow: 'hidden',\n clip: 'rect(0 0 0 0)',\n whiteSpace: 'nowrap',\n }}\n >\n {label}\n </RadixDialog.Title>\n {children}\n </RadixDialog.Content>\n </div>\n </RadixDialog.Portal>\n </RadixDialog.Root>\n )\n}\n\nexport interface DialogProps extends DialogBaseProps {\n maxWidth?: number | false\n className?: string\n style?: CSSProperties\n}\n\n/**\n * 확인이 필요한 동작에 쓰는 중앙 다이얼로그.\n *\n * 구 `StandardModal`(RN)과 `AppDialog`(웹)를 하나의 계약으로 합쳤다.\n * 버튼을 직접 구성하고 싶으면 `children` 을 쓰고, 그러면 `cancel`/`confirm` 은 무시된다.\n */\nexport function Dialog({\n open,\n onOpenChange,\n dismissible = true,\n title,\n description,\n cancel,\n confirm,\n children,\n maxWidth = LAYOUT.maxWidth - LAYOUT.dialog.insetX * 2,\n className,\n style,\n}: DialogProps) {\n useDim(open)\n\n return (\n <RadixDialog.Root open={open} onOpenChange={onOpenChange}>\n <RadixDialog.Portal>\n <RadixDialog.Overlay style={overlayStyle} />\n <div style={{ ...centered(maxWidth), top: 0, bottom: 0, alignItems: 'center' }}>\n <RadixDialog.Content\n className={className}\n onPointerDownOutside={(e) => {\n if (!dismissible) e.preventDefault()\n }}\n onEscapeKeyDown={(e) => {\n if (!dismissible) e.preventDefault()\n }}\n style={{\n pointerEvents: 'auto',\n width: `calc(100% - ${LAYOUT.dialog.insetX * 2}px)`,\n ...(maxWidth === false ? null : { maxWidth }),\n borderRadius: LAYOUT.dialog.radius,\n padding: '26px 22px 18px',\n display: 'flex',\n flexDirection: 'column',\n gap: 12,\n textAlign: 'center',\n ...style,\n }}\n >\n <RadixDialog.Title style={{ margin: 0 }}>{title}</RadixDialog.Title>\n {description !== undefined && (\n <RadixDialog.Description style={{ margin: 0 }}>{description}</RadixDialog.Description>\n )}\n\n {children ?? (\n <div style={{ display: 'flex', gap: 10, marginTop: 6 }}>\n {cancel && (\n <RadixDialog.Close asChild>\n <button type=\"button\" style={{ flex: 1 }} onClick={cancel.onPress}>\n {cancel.label}\n </button>\n </RadixDialog.Close>\n )}\n {confirm && (\n <button\n type=\"button\"\n style={{ flex: 1 }}\n data-tone={confirm.tone ?? 'default'}\n onClick={() => {\n confirm.onPress?.()\n onOpenChange(false)\n }}\n >\n {confirm.label}\n </button>\n )}\n </div>\n )}\n </RadixDialog.Content>\n </div>\n </RadixDialog.Portal>\n </RadixDialog.Root>\n )\n}\n"],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@openiot-devteam/layout",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "OpenIoT 모바일 레이아웃 — 웹(Next)과 RN(Expo)에서 같은 API로 쓰는 화면 골격",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/openiot-devteam/000-design-system.git",
|
|
8
|
+
"directory": "packages/layout"
|
|
9
|
+
},
|
|
10
|
+
"license": "UNLICENSED",
|
|
11
|
+
"type": "module",
|
|
12
|
+
"sideEffects": false,
|
|
13
|
+
"files": [
|
|
14
|
+
"dist"
|
|
15
|
+
],
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"react-native": {
|
|
19
|
+
"types": "./dist/native.d.mts",
|
|
20
|
+
"import": "./dist/native.mjs"
|
|
21
|
+
},
|
|
22
|
+
"types": "./dist/index.d.mts",
|
|
23
|
+
"import": "./dist/index.mjs"
|
|
24
|
+
},
|
|
25
|
+
"./tokens": {
|
|
26
|
+
"types": "./dist/tokens.d.mts",
|
|
27
|
+
"import": "./dist/tokens.mjs"
|
|
28
|
+
},
|
|
29
|
+
"./package.json": "./package.json"
|
|
30
|
+
},
|
|
31
|
+
"scripts": {
|
|
32
|
+
"build": "tsdown",
|
|
33
|
+
"typecheck": "tsc --noEmit",
|
|
34
|
+
"clean": "rm -rf dist .turbo"
|
|
35
|
+
},
|
|
36
|
+
"publishConfig": {
|
|
37
|
+
"access": "public"
|
|
38
|
+
},
|
|
39
|
+
"peerDependencies": {
|
|
40
|
+
"react": "^19.0.0",
|
|
41
|
+
"react-dom": "^19.0.0",
|
|
42
|
+
"react-native": ">=0.76",
|
|
43
|
+
"react-native-safe-area-context": ">=4.10"
|
|
44
|
+
},
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"@radix-ui/react-dialog": "^1.1.17"
|
|
47
|
+
},
|
|
48
|
+
"peerDependenciesMeta": {
|
|
49
|
+
"react-dom": {
|
|
50
|
+
"optional": true
|
|
51
|
+
},
|
|
52
|
+
"react-native": {
|
|
53
|
+
"optional": true
|
|
54
|
+
},
|
|
55
|
+
"react-native-safe-area-context": {
|
|
56
|
+
"optional": true
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
"devDependencies": {
|
|
60
|
+
"react-native": "0.83.2",
|
|
61
|
+
"react-native-safe-area-context": "^5.6.0"
|
|
62
|
+
}
|
|
63
|
+
}
|