@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.
Files changed (40) hide show
  1. package/README.md +147 -0
  2. package/dist/dim-bridge.d.mts +21 -0
  3. package/dist/dim-bridge.d.mts.map +1 -0
  4. package/dist/dim-bridge.mjs +22 -0
  5. package/dist/dim-bridge.mjs.map +1 -0
  6. package/dist/index.d.mts +7 -0
  7. package/dist/index.mjs +6 -0
  8. package/dist/native/FitLayout.d.mts +26 -0
  9. package/dist/native/FitLayout.d.mts.map +1 -0
  10. package/dist/native/FitLayout.mjs +80 -0
  11. package/dist/native/FitLayout.mjs.map +1 -0
  12. package/dist/native/ScreenLayout.d.mts +42 -0
  13. package/dist/native/ScreenLayout.d.mts.map +1 -0
  14. package/dist/native/ScreenLayout.mjs +90 -0
  15. package/dist/native/ScreenLayout.mjs.map +1 -0
  16. package/dist/native/overlays.d.mts +43 -0
  17. package/dist/native/overlays.d.mts.map +1 -0
  18. package/dist/native/overlays.mjs +170 -0
  19. package/dist/native/overlays.mjs.map +1 -0
  20. package/dist/native.d.mts +7 -0
  21. package/dist/native.mjs +6 -0
  22. package/dist/tokens.d.mts +43 -0
  23. package/dist/tokens.d.mts.map +1 -0
  24. package/dist/tokens.mjs +50 -0
  25. package/dist/tokens.mjs.map +1 -0
  26. package/dist/types.d.mts +91 -0
  27. package/dist/types.d.mts.map +1 -0
  28. package/dist/web/FitLayout.d.mts +55 -0
  29. package/dist/web/FitLayout.d.mts.map +1 -0
  30. package/dist/web/FitLayout.mjs +111 -0
  31. package/dist/web/FitLayout.mjs.map +1 -0
  32. package/dist/web/ScreenLayout.d.mts +61 -0
  33. package/dist/web/ScreenLayout.d.mts.map +1 -0
  34. package/dist/web/ScreenLayout.mjs +118 -0
  35. package/dist/web/ScreenLayout.mjs.map +1 -0
  36. package/dist/web/overlays.d.mts +58 -0
  37. package/dist/web/overlays.d.mts.map +1 -0
  38. package/dist/web/overlays.mjs +173 -0
  39. package/dist/web/overlays.mjs.map +1 -0
  40. 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":";;;;;;;;AASA,SAAS,OAAO,MAAe;CAC7B,gBAAgB;EACd,IAAI,CAAC,MAAM;EACX,WAAW;EACX,OAAO;CACT,GAAG,CAAC,IAAI,CAAC;AACX;AAEA,MAAM,eAA8B;CAClC,UAAU;CACV,OAAO;CACP,QAAQ;CACR,iBAAiB,iBAAiB,OAAO,WAAW;AACtD;;AAGA,SAAS,SAAS,UAAyC;CACzD,OAAO;EACL,UAAU;EACV,MAAM;EACN,OAAO;EACP,QAAQ;EACR,SAAS;EACT,gBAAgB;EAChB,GAAI,aAAa,QAAQ,OAAO,EAAE,eAAe,OAAgB;CACnE;AACF;;;;;;;;;;;;AAoBA,SAAgB,YAAY,EAC1B,MACA,cACA,cAAc,MACd,OACA,UACA,WAAW,OAAO,UAClB,WACA,SACmB;CACnB,OAAO,IAAI;CAEX,OACE,oBAAC,YAAY,MAAb;EAAwB;EAAoB;YAC1C,qBAAC,YAAY,QAAb,EAAA,UAAA,CACE,oBAAC,YAAY,SAAb,EAAqB,OAAO,aAAe,CAAA,GAC3C,oBAAC,OAAD;GAAK,OAAO;IAAE,GAAG,SAAS,QAAQ;IAAG,QAAQ;GAAE;aAC7C,qBAAC,YAAY,SAAb;IACa;IACX,cAAY;IACZ,uBAAuB,MAAM;KAC3B,IAAI,CAAC,aAAa,EAAE,eAAe;IACrC;IACA,kBAAkB,MAAM;KACtB,IAAI,CAAC,aAAa,EAAE,eAAe;IACrC;IACA,OAAO;KACL,eAAe;KACf,OAAO;KACP,GAAI,aAAa,QAAQ,OAAO,EAAE,SAAS;KAC3C,qBAAqB,OAAO;KAC5B,sBAAsB,OAAO;KAC7B,aAAa,OAAO,QAAQ;KAC5B,cAAc,OAAO,QAAQ;KAC7B,YAAY;KACZ,eAAe;KACf,WAAW;KACX,WAAW;KACX,GAAG;IACL;cAtBF,CAwBE,oBAAC,YAAY,OAAb;KACE,OAAO;MACL,UAAU;MACV,OAAO;MACP,QAAQ;MACR,UAAU;MACV,MAAM;MACN,YAAY;KACd;eAEC;IACgB,CAAA,GAClB,QACkB;;EAClB,CAAA,CACa,EAAA,CAAA;CACJ,CAAA;AAEtB;;;;;;;AAcA,SAAgB,OAAO,EACrB,MACA,cACA,cAAc,MACd,OACA,aACA,QACA,SACA,UACA,WAAW,OAAO,WAAW,OAAO,OAAO,SAAS,GACpD,WACA,SACc;CACd,OAAO,IAAI;CAEX,OACE,oBAAC,YAAY,MAAb;EAAwB;EAAoB;YAC1C,qBAAC,YAAY,QAAb,EAAA,UAAA,CACE,oBAAC,YAAY,SAAb,EAAqB,OAAO,aAAe,CAAA,GAC3C,oBAAC,OAAD;GAAK,OAAO;IAAE,GAAG,SAAS,QAAQ;IAAG,KAAK;IAAG,QAAQ;IAAG,YAAY;GAAS;aAC3E,qBAAC,YAAY,SAAb;IACa;IACX,uBAAuB,MAAM;KAC3B,IAAI,CAAC,aAAa,EAAE,eAAe;IACrC;IACA,kBAAkB,MAAM;KACtB,IAAI,CAAC,aAAa,EAAE,eAAe;IACrC;IACA,OAAO;KACL,eAAe;KACf,OAAO,eAAe,OAAO,OAAO,SAAS,EAAE;KAC/C,GAAI,aAAa,QAAQ,OAAO,EAAE,SAAS;KAC3C,cAAc,OAAO,OAAO;KAC5B,SAAS;KACT,SAAS;KACT,eAAe;KACf,KAAK;KACL,WAAW;KACX,GAAG;IACL;cAnBF;KAqBE,oBAAC,YAAY,OAAb;MAAmB,OAAO,EAAE,QAAQ,EAAE;gBAAI;KAAyB,CAAA;KAClE,gBAAgB,KAAA,KACf,oBAAC,YAAY,aAAb;MAAyB,OAAO,EAAE,QAAQ,EAAE;gBAAI;KAAqC,CAAA;KAGtF,YACC,qBAAC,OAAD;MAAK,OAAO;OAAE,SAAS;OAAQ,KAAK;OAAI,WAAW;MAAE;gBAArD,CACG,UACC,oBAAC,YAAY,OAAb;OAAmB,SAAA;iBACjB,oBAAC,UAAD;QAAQ,MAAK;QAAS,OAAO,EAAE,MAAM,EAAE;QAAG,SAAS,OAAO;kBACvD,OAAO;OACF,CAAA;MACS,CAAA,GAEpB,WACC,oBAAC,UAAD;OACE,MAAK;OACL,OAAO,EAAE,MAAM,EAAE;OACjB,aAAW,QAAQ,QAAQ;OAC3B,eAAe;QACb,QAAQ,UAAU;QAClB,aAAa,KAAK;OACpB;iBAEC,QAAQ;MACH,CAAA,CAEP;;IAEY;;EAClB,CAAA,CACa,EAAA,CAAA;CACJ,CAAA;AAEtB"}
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
+ }