cross-app-launcher 0.0.0-beta.1
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 +209 -0
- package/dist/index.cjs +378 -0
- package/dist/index.d.cts +52 -0
- package/dist/index.d.ts +52 -0
- package/dist/index.js +338 -0
- package/package.json +59 -0
package/README.md
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
# app-launcher
|
|
2
|
+
|
|
3
|
+
CROSS 생태계 서비스 전체에서 사용하는 통합 앱 런처 메뉴 컴포넌트.
|
|
4
|
+
|
|
5
|
+
S3에 호스팅된 글로벌 메뉴 메타데이터를 fetch하여 일관된 UI로 렌더링한다. Desktop에서는 Popover, Mobile에서는 오른쪽 Drawer로 자동 전환된다.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install cross-app-launcher
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
### Peer Dependencies
|
|
14
|
+
|
|
15
|
+
소비자 프로젝트에 아래 패키지가 이미 설치되어 있어야 한다.
|
|
16
|
+
|
|
17
|
+
- `react` >= 18
|
|
18
|
+
- `react-dom` >= 18
|
|
19
|
+
- `@tanstack/react-query` >= 5
|
|
20
|
+
|
|
21
|
+
## Quick Start
|
|
22
|
+
|
|
23
|
+
```tsx
|
|
24
|
+
import {
|
|
25
|
+
AppLauncher,
|
|
26
|
+
AppLauncherTrigger,
|
|
27
|
+
AppLauncherContent,
|
|
28
|
+
} from "cross-app-launcher";
|
|
29
|
+
|
|
30
|
+
function MyNav() {
|
|
31
|
+
return (
|
|
32
|
+
<AppLauncher>
|
|
33
|
+
<AppLauncherTrigger />
|
|
34
|
+
<AppLauncherContent />
|
|
35
|
+
</AppLauncher>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
모든 prop은 optional이며, 별도의 CSS import 없이 스타일이 자동 적용된다.
|
|
41
|
+
|
|
42
|
+
### Next.js에서 사용
|
|
43
|
+
|
|
44
|
+
```tsx
|
|
45
|
+
import { useRouter } from "next/navigation";
|
|
46
|
+
|
|
47
|
+
function MyNav() {
|
|
48
|
+
const router = useRouter();
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<AppLauncher
|
|
52
|
+
env={process.env.NEXT_PUBLIC_ENV as "dev" | "stage" | "production"}
|
|
53
|
+
onNavigate={(path) => router.push(path)}
|
|
54
|
+
>
|
|
55
|
+
<AppLauncherTrigger />
|
|
56
|
+
<AppLauncherContent />
|
|
57
|
+
</AppLauncher>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Components
|
|
63
|
+
|
|
64
|
+
### `<AppLauncher>`
|
|
65
|
+
|
|
66
|
+
Root 컴포넌트. Context Provider + Popover/Drawer Root 역할.
|
|
67
|
+
|
|
68
|
+
| Prop | Type | Default | Description |
|
|
69
|
+
| --- | --- | --- | --- |
|
|
70
|
+
| `env` | `'dev' \| 'stage' \| 'production'` | `'production'` | 환경에 따라 메뉴 아이템의 URL이 결정된다 |
|
|
71
|
+
| `theme` | `'dark' \| 'light'` | `'dark'` | 다크/라이트 테마 |
|
|
72
|
+
| `mobileBreakpoint` | `number` | `768` | 이 px 이하에서 Drawer로 전환 |
|
|
73
|
+
| `onNavigate` | `(path: string) => void` | - | 같은 도메인 내 이동 시 호출. 없으면 `window.location.href`로 풀 리로드 |
|
|
74
|
+
| `domain` | `string` | `window.location.hostname` | 내부/외부 링크 판별 기준 도메인. 보통 넘기지 않아도 된다 |
|
|
75
|
+
|
|
76
|
+
### `<AppLauncherTrigger>`
|
|
77
|
+
|
|
78
|
+
Trigger 버튼. children 없이 사용하면 기본 dots grid 아이콘 버튼이 렌더링된다. 열림 상태에서 배경색이 자동 변경된다.
|
|
79
|
+
|
|
80
|
+
| Prop | Type | Default | Description |
|
|
81
|
+
| --- | --- | --- | --- |
|
|
82
|
+
| `asChild` | `boolean` | `true` | `true`이면 자식 엘리먼트를 trigger로 사용 (Radix 패턴) |
|
|
83
|
+
|
|
84
|
+
```tsx
|
|
85
|
+
// 기본 트리거 사용
|
|
86
|
+
<AppLauncherTrigger />
|
|
87
|
+
|
|
88
|
+
// 커스텀 트리거 사용
|
|
89
|
+
<AppLauncherTrigger>
|
|
90
|
+
<button>Menu</button>
|
|
91
|
+
</AppLauncherTrigger>
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### `<AppLauncherContent>`
|
|
95
|
+
|
|
96
|
+
메뉴 콘텐츠. 2열 그리드 레이아웃. 패키지가 디자인을 통제한다.
|
|
97
|
+
|
|
98
|
+
| Prop | Type | Default | Description |
|
|
99
|
+
| --- | --- | --- | --- |
|
|
100
|
+
| `align` | `'start' \| 'center' \| 'end'` | `'end'` | 팝오버 정렬 위치 (Desktop only) |
|
|
101
|
+
| `sideOffset` | `number` | `12` | trigger와의 간격 px (Desktop) |
|
|
102
|
+
| `className` | `string` | - | 추가 CSS 클래스 |
|
|
103
|
+
|
|
104
|
+
## Link Handling
|
|
105
|
+
|
|
106
|
+
패키지가 내부/외부 링크를 자동으로 판별하여 처리한다.
|
|
107
|
+
|
|
108
|
+
- **같은 도메인** → `onNavigate(path)` 호출 (없으면 `window.location.href`로 이동)
|
|
109
|
+
- **다른 도메인** → `window.open`으로 새 창 열기
|
|
110
|
+
|
|
111
|
+
도메인 판별은 `window.location.hostname` 기준이므로 `domain` prop을 넘기지 않아도 된다.
|
|
112
|
+
|
|
113
|
+
### Active State
|
|
114
|
+
|
|
115
|
+
현재 페이지에 해당하는 메뉴 아이템에 `data-active="true"` 속성이 설정된다. URL의 locale prefix (`/en/` 등)와 dynamic path (`/gametoken/CROSS/CROMx` 등)가 포함된 경우에도 정상 판별된다.
|
|
116
|
+
|
|
117
|
+
## Responsive
|
|
118
|
+
|
|
119
|
+
- **Desktop** (`> mobileBreakpoint`): Popover로 렌더링
|
|
120
|
+
- **Mobile** (`<= mobileBreakpoint`): 오른쪽 Drawer로 렌더링 (뒤로가기 화살표 + 닫기 버튼 포함)
|
|
121
|
+
|
|
122
|
+
소비자는 별도의 분기 처리 없이 패키지가 자동으로 전환한다. 드로어에서 드래그 시 서비스 아이템 클릭이 방지된다.
|
|
123
|
+
|
|
124
|
+
## Data Source
|
|
125
|
+
|
|
126
|
+
메뉴 데이터는 S3에서 자동으로 fetch된다.
|
|
127
|
+
|
|
128
|
+
- `@tanstack/react-query`의 `useQuery` 사용
|
|
129
|
+
- 캐시 전략: `no-cache` (항상 최신 데이터)
|
|
130
|
+
- 메뉴 아이템은 `order` 필드 기준으로 정렬
|
|
131
|
+
|
|
132
|
+
### `useGlobalMenu` Hook
|
|
133
|
+
|
|
134
|
+
패키지에서 `useGlobalMenu` hook도 export한다.
|
|
135
|
+
|
|
136
|
+
```tsx
|
|
137
|
+
import { useGlobalMenu } from "cross-app-launcher";
|
|
138
|
+
|
|
139
|
+
function MyComponent() {
|
|
140
|
+
const { data, isLoading } = useGlobalMenu();
|
|
141
|
+
// data.items: GlobalMenuItem[]
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## Styling
|
|
146
|
+
|
|
147
|
+
- Vanilla CSS + CSS Custom Properties
|
|
148
|
+
- 클래스명은 `al-` 접두사로 네임스페이스 충돌 방지
|
|
149
|
+
- `injectStyle`로 CSS가 JS에 포함되어 별도 CSS import 불필요
|
|
150
|
+
- 테마는 `data-theme` 속성으로 전환
|
|
151
|
+
|
|
152
|
+
### CSS Custom Properties
|
|
153
|
+
|
|
154
|
+
필요 시 CSS 변수를 override하여 커스텀 가능:
|
|
155
|
+
|
|
156
|
+
| Variable | Description |
|
|
157
|
+
| --- | --- |
|
|
158
|
+
| `--al-bg` | 배경색 |
|
|
159
|
+
| `--al-bg-hover` | hover 배경색 |
|
|
160
|
+
| `--al-text` | 기본 텍스트 색 |
|
|
161
|
+
| `--al-text-secondary` | 보조 텍스트 색 |
|
|
162
|
+
| `--al-border` | 테두리 색 |
|
|
163
|
+
| `--al-shadow` | 박스 그림자 |
|
|
164
|
+
| `--al-badge-bg` | 뱃지 배경색 |
|
|
165
|
+
| `--al-badge-text` | 뱃지 텍스트 색 |
|
|
166
|
+
| `--al-badge-new-text` | New 뱃지 텍스트 색 |
|
|
167
|
+
| `--al-drawer-overlay` | Drawer 오버레이 색 |
|
|
168
|
+
|
|
169
|
+
## Types
|
|
170
|
+
|
|
171
|
+
```ts
|
|
172
|
+
type Environment = "dev" | "stage" | "production";
|
|
173
|
+
|
|
174
|
+
interface GlobalMenuItem {
|
|
175
|
+
id: string;
|
|
176
|
+
label: string;
|
|
177
|
+
description: string;
|
|
178
|
+
url: { dev: string; stage: string; production: string };
|
|
179
|
+
iconUrl: string;
|
|
180
|
+
order: number;
|
|
181
|
+
type: string;
|
|
182
|
+
badge: string | null;
|
|
183
|
+
isNew: boolean;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
interface GlobalMenu {
|
|
187
|
+
version: string;
|
|
188
|
+
items: GlobalMenuItem[];
|
|
189
|
+
}
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
## Architecture
|
|
193
|
+
|
|
194
|
+
```
|
|
195
|
+
AppLauncher (Provider + Popover.Root / Drawer.Root)
|
|
196
|
+
├── AppLauncherTrigger (headless - 기본 아이콘 또는 소비자 제공)
|
|
197
|
+
└── AppLauncherContent (styled - 패키지 통제)
|
|
198
|
+
├── Desktop → Popover.Content (2열 그리드)
|
|
199
|
+
└── Mobile → Drawer.Content (뒤로가기 + 닫기 헤더, 1열 그리드)
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
## Development
|
|
203
|
+
|
|
204
|
+
```bash
|
|
205
|
+
npm install
|
|
206
|
+
npm run build # 빌드
|
|
207
|
+
npm run dev # watch 모드
|
|
208
|
+
npm run typecheck # 타입 체크
|
|
209
|
+
```
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
AppLauncher: () => AppLauncher,
|
|
34
|
+
AppLauncherContent: () => AppLauncherContent,
|
|
35
|
+
AppLauncherTrigger: () => AppLauncherTrigger,
|
|
36
|
+
useGlobalMenu: () => useGlobalMenu
|
|
37
|
+
});
|
|
38
|
+
module.exports = __toCommonJS(index_exports);
|
|
39
|
+
|
|
40
|
+
// src/components/AppLauncher.tsx
|
|
41
|
+
var React = __toESM(require("react"), 1);
|
|
42
|
+
var Popover = __toESM(require("@radix-ui/react-popover"), 1);
|
|
43
|
+
var import_vaul = require("vaul");
|
|
44
|
+
|
|
45
|
+
// src/context.ts
|
|
46
|
+
var import_react = require("react");
|
|
47
|
+
var AppLauncherContext = (0, import_react.createContext)(
|
|
48
|
+
null
|
|
49
|
+
);
|
|
50
|
+
function useAppLauncherContext() {
|
|
51
|
+
const ctx = (0, import_react.useContext)(AppLauncherContext);
|
|
52
|
+
if (!ctx) {
|
|
53
|
+
throw new Error(
|
|
54
|
+
"AppLauncher compound components must be used within <AppLauncher>"
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
return ctx;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// src/hooks/useMediaQuery.ts
|
|
61
|
+
var import_react2 = require("react");
|
|
62
|
+
function useMediaQuery(query) {
|
|
63
|
+
const [matches, setMatches] = (0, import_react2.useState)(false);
|
|
64
|
+
(0, import_react2.useEffect)(() => {
|
|
65
|
+
const mql = window.matchMedia(query);
|
|
66
|
+
setMatches(mql.matches);
|
|
67
|
+
const handler = (e) => setMatches(e.matches);
|
|
68
|
+
mql.addEventListener("change", handler);
|
|
69
|
+
return () => mql.removeEventListener("change", handler);
|
|
70
|
+
}, [query]);
|
|
71
|
+
return matches;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// src/components/AppLauncher.tsx
|
|
75
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
76
|
+
function AppLauncher({
|
|
77
|
+
env = "production",
|
|
78
|
+
theme = "dark",
|
|
79
|
+
mobileBreakpoint = 768,
|
|
80
|
+
domain,
|
|
81
|
+
onNavigate,
|
|
82
|
+
children
|
|
83
|
+
}) {
|
|
84
|
+
const isMobile = useMediaQuery(`(max-width: ${mobileBreakpoint}px)`);
|
|
85
|
+
const [open, setOpen] = React.useState(false);
|
|
86
|
+
const close = React.useCallback(() => setOpen(false), []);
|
|
87
|
+
const contextValue = React.useMemo(
|
|
88
|
+
() => ({ env, theme, isMobile, open, domain, onNavigate, close }),
|
|
89
|
+
[env, theme, isMobile, open, domain, onNavigate, close]
|
|
90
|
+
);
|
|
91
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AppLauncherContext.Provider, { value: contextValue, children: isMobile ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
92
|
+
import_vaul.Drawer.Root,
|
|
93
|
+
{
|
|
94
|
+
direction: "right",
|
|
95
|
+
open,
|
|
96
|
+
onOpenChange: setOpen,
|
|
97
|
+
children
|
|
98
|
+
},
|
|
99
|
+
"drawer"
|
|
100
|
+
) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Popover.Root, { open, onOpenChange: setOpen, children }, "popover") });
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// src/components/AppLauncherTrigger.tsx
|
|
104
|
+
var Popover2 = __toESM(require("@radix-ui/react-popover"), 1);
|
|
105
|
+
var import_vaul2 = require("vaul");
|
|
106
|
+
|
|
107
|
+
// src/components/icons.tsx
|
|
108
|
+
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
109
|
+
function DotsGridIcon({ size = 18 }) {
|
|
110
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
|
|
111
|
+
"svg",
|
|
112
|
+
{
|
|
113
|
+
className: "al-trigger-icon",
|
|
114
|
+
width: size,
|
|
115
|
+
height: size,
|
|
116
|
+
viewBox: "0 0 18 18",
|
|
117
|
+
fill: "none",
|
|
118
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
119
|
+
children: [
|
|
120
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("rect", { width: "4", height: "4", fill: "currentColor" }),
|
|
121
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("rect", { x: "7", width: "4", height: "4", fill: "currentColor" }),
|
|
122
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("rect", { x: "14", width: "4", height: "4", fill: "currentColor" }),
|
|
123
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("rect", { y: "7", width: "4", height: "4", fill: "currentColor" }),
|
|
124
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("rect", { x: "7", y: "7", width: "4", height: "4", fill: "currentColor" }),
|
|
125
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("rect", { x: "14", y: "7", width: "4", height: "4", fill: "currentColor" }),
|
|
126
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("rect", { y: "14", width: "4", height: "4", fill: "currentColor" }),
|
|
127
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("rect", { x: "7", y: "14", width: "4", height: "4", fill: "currentColor" }),
|
|
128
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("rect", { x: "14", y: "14", width: "4", height: "4", fill: "currentColor" })
|
|
129
|
+
]
|
|
130
|
+
}
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
function BackArrowIcon() {
|
|
134
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
135
|
+
"svg",
|
|
136
|
+
{
|
|
137
|
+
width: "24",
|
|
138
|
+
height: "24",
|
|
139
|
+
viewBox: "0 0 24 24",
|
|
140
|
+
fill: "none",
|
|
141
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
142
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
143
|
+
"path",
|
|
144
|
+
{
|
|
145
|
+
d: "M12 19L5 12M5 12L12 5M5 12H19",
|
|
146
|
+
stroke: "currentColor",
|
|
147
|
+
strokeWidth: "1.5",
|
|
148
|
+
strokeLinecap: "round",
|
|
149
|
+
strokeLinejoin: "round"
|
|
150
|
+
}
|
|
151
|
+
)
|
|
152
|
+
}
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
function CloseIcon() {
|
|
156
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
157
|
+
"svg",
|
|
158
|
+
{
|
|
159
|
+
width: "20",
|
|
160
|
+
height: "20",
|
|
161
|
+
viewBox: "0 0 20 20",
|
|
162
|
+
fill: "none",
|
|
163
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
164
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
165
|
+
"path",
|
|
166
|
+
{
|
|
167
|
+
d: "M15 5L5 15M5 5L15 15",
|
|
168
|
+
stroke: "currentColor",
|
|
169
|
+
strokeWidth: "1.5",
|
|
170
|
+
strokeLinecap: "round",
|
|
171
|
+
strokeLinejoin: "round"
|
|
172
|
+
}
|
|
173
|
+
)
|
|
174
|
+
}
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// src/components/AppLauncherTrigger.tsx
|
|
179
|
+
var import_jsx_runtime3 = require("react/jsx-runtime");
|
|
180
|
+
function AppLauncherTrigger({
|
|
181
|
+
asChild,
|
|
182
|
+
children
|
|
183
|
+
}) {
|
|
184
|
+
const { isMobile, theme } = useAppLauncherContext();
|
|
185
|
+
const hasChildren = children != null;
|
|
186
|
+
const shouldAsChild = asChild ?? true;
|
|
187
|
+
const triggerContent = hasChildren ? children : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", className: "al-trigger-btn", "data-theme": theme, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(DotsGridIcon, { size: isMobile ? 12 : 18 }) });
|
|
188
|
+
if (isMobile) {
|
|
189
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_vaul2.Drawer.Trigger, { asChild: shouldAsChild, children: triggerContent });
|
|
190
|
+
}
|
|
191
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Popover2.Trigger, { asChild: shouldAsChild, children: triggerContent });
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// src/components/AppLauncherContent.tsx
|
|
195
|
+
var React2 = __toESM(require("react"), 1);
|
|
196
|
+
var Popover3 = __toESM(require("@radix-ui/react-popover"), 1);
|
|
197
|
+
var import_vaul3 = require("vaul");
|
|
198
|
+
|
|
199
|
+
// src/hooks/useGlobalMenu.ts
|
|
200
|
+
var import_react_query = require("@tanstack/react-query");
|
|
201
|
+
var GLOBAL_MENU_URL = "https://contents.crosstoken.io/frontend/common/app-launcher/app-launcher.json";
|
|
202
|
+
function useGlobalMenu() {
|
|
203
|
+
return (0, import_react_query.useQuery)({
|
|
204
|
+
queryKey: ["global-menu"],
|
|
205
|
+
queryFn: async () => {
|
|
206
|
+
const res = await fetch(GLOBAL_MENU_URL, { cache: "no-store" });
|
|
207
|
+
if (!res.ok) {
|
|
208
|
+
throw new Error(`Failed to fetch global menu: ${res.status}`);
|
|
209
|
+
}
|
|
210
|
+
return res.json();
|
|
211
|
+
},
|
|
212
|
+
staleTime: 0,
|
|
213
|
+
gcTime: 0,
|
|
214
|
+
refetchOnMount: true
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// #style-inject:#style-inject
|
|
219
|
+
function styleInject(css, { insertAt } = {}) {
|
|
220
|
+
if (!css || typeof document === "undefined") return;
|
|
221
|
+
const head = document.head || document.getElementsByTagName("head")[0];
|
|
222
|
+
const style = document.createElement("style");
|
|
223
|
+
style.type = "text/css";
|
|
224
|
+
if (insertAt === "top") {
|
|
225
|
+
if (head.firstChild) {
|
|
226
|
+
head.insertBefore(style, head.firstChild);
|
|
227
|
+
} else {
|
|
228
|
+
head.appendChild(style);
|
|
229
|
+
}
|
|
230
|
+
} else {
|
|
231
|
+
head.appendChild(style);
|
|
232
|
+
}
|
|
233
|
+
if (style.styleSheet) {
|
|
234
|
+
style.styleSheet.cssText = css;
|
|
235
|
+
} else {
|
|
236
|
+
style.appendChild(document.createTextNode(css));
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// src/styles.css
|
|
241
|
+
styleInject("[data-theme=dark] {\n --al-bg: #1e232e;\n --al-bg-hover: #252b39;\n --al-text: hsla(200, 19%, 94%, 1);\n --al-text-secondary: hsla(200, 10%, 70%, 1);\n --al-border: #3b4153;\n --al-shadow: 0 2px 8px -1px rgba(0, 0, 0, 0.15);\n --al-badge-bg: #123f3c;\n --al-badge-text: hsla(168, 85%, 47%, 1);\n --al-badge-new-text: #000;\n --al-drawer-overlay: rgba(0, 0, 0, 0.6);\n}\n[data-theme=light] {\n --al-bg: #fff;\n --al-bg-hover: #f3f6f8;\n --al-text: hsla(221, 21%, 15%, 1);\n --al-text-secondary: hsla(220, 10%, 40%, 1);\n --al-border: #ecf0f2;\n --al-shadow: 0 2px 8px -1px rgba(0, 0, 0, 0.15);\n --al-badge-bg: #cdf4ed;\n --al-badge-text: hsla(170, 82%, 32%, 1);\n --al-badge-new-text: #fff;\n --al-drawer-overlay: rgba(0, 0, 0, 0.4);\n}\n.al-trigger-btn {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n padding: 8px;\n border: none;\n background: transparent;\n border-radius: 8px;\n cursor: pointer;\n transition: background-color 0.15s ease;\n}\n.al-trigger-btn:hover {\n background: rgba(128, 128, 128, 0.1);\n}\n.al-trigger-btn[data-theme=dark] {\n color: hsla(200, 19%, 94%, 1);\n}\n.al-trigger-btn[data-theme=light] {\n color: hsla(221, 21%, 15%, 1);\n}\n[data-state=open] > .al-trigger-btn[data-theme=dark],\n.al-trigger-btn[data-state=open][data-theme=dark] {\n border-radius: 14px;\n background: #161b22;\n}\n[data-state=open] > .al-trigger-btn[data-theme=light],\n.al-trigger-btn[data-state=open][data-theme=light] {\n border-radius: 14px;\n background: #ecf0f2;\n}\n.al-popover-content {\n z-index: 50;\n border-radius: 12px;\n background: var(--al-bg);\n border: 1px solid var(--al-border);\n box-shadow: var(--al-shadow);\n animation: al-fade-in 0.15s ease-out;\n max-height: 80vh;\n overflow-x: hidden;\n overflow-y: auto;\n}\n@keyframes al-fade-in {\n from {\n opacity: 0;\n transform: translateY(-4px);\n }\n to {\n opacity: 1;\n transform: translateY(0);\n }\n}\n.al-drawer-overlay {\n position: fixed;\n inset: 0;\n z-index: 49;\n background: var(--al-drawer-overlay);\n}\n.al-drawer-content {\n position: fixed;\n top: 0;\n right: 0;\n bottom: 0;\n z-index: 50;\n width: 360px;\n max-width: 100vw;\n background: var(--al-bg);\n overflow-x: hidden;\n overflow-y: hidden;\n display: flex;\n flex-direction: column;\n box-sizing: border-box;\n}\n.al-drawer-body {\n flex: 1;\n overflow-y: auto;\n overscroll-behavior-y: contain;\n min-height: 0;\n}\n.al-drawer-content .al-grid {\n grid-template-columns: 1fr;\n max-width: 100%;\n overflow: hidden;\n flex-shrink: 0;\n gap: 4px;\n}\n.al-drawer-content .al-grid-item {\n max-width: 100%;\n overflow: hidden;\n}\n.al-drawer-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 16px 20px;\n flex-shrink: 0;\n}\n.al-drawer-title {\n font-family: Inter, sans-serif;\n font-size: 18px;\n font-weight: 700;\n color: var(--al-text);\n margin: 0;\n}\n.al-drawer-close {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n padding: 4px;\n border: none;\n background: transparent;\n border-radius: 4px;\n cursor: pointer;\n color: var(--al-text);\n transition: opacity 0.15s ease;\n}\n.al-drawer-close:hover {\n opacity: 0.7;\n}\n.al-grid {\n display: grid;\n grid-template-columns: repeat(2, 1fr);\n gap: 4px;\n padding: 8px;\n}\n.al-grid-item {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 10px;\n padding: 16px 12px;\n border: none;\n background: transparent;\n border-radius: 8px;\n cursor: pointer;\n transition: background-color 0.15s ease;\n color: var(--al-text);\n text-align: left;\n font-family: Inter, sans-serif;\n flex-shrink: 0;\n}\n.al-grid-item:hover,\n.al-grid-item[data-active=true] {\n background: var(--al-bg-hover);\n}\n.al-grid-item-left {\n display: flex;\n align-items: center;\n gap: 8px;\n min-width: 0;\n overflow: hidden;\n}\n.al-grid-item-right {\n display: flex;\n align-items: center;\n gap: 6px;\n flex-shrink: 0;\n}\n.al-grid-item-icon {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 24px;\n height: 24px;\n flex-shrink: 0;\n}\n.al-grid-item-icon img {\n width: 100%;\n height: 100%;\n object-fit: contain;\n}\n.al-grid-item-label {\n font-family: Inter, sans-serif;\n font-size: 16px;\n font-weight: 500;\n line-height: 150%;\n letter-spacing: -0.16px;\n color: var(--al-text);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n.al-badge {\n display: flex;\n padding: 1px 4px 2px 4px;\n justify-content: center;\n align-items: center;\n gap: 2px;\n border-radius: 3px;\n background: var(--al-badge-bg);\n color: var(--al-badge-text);\n font-family:\n Pretendard,\n Inter,\n sans-serif;\n font-size: 10px;\n font-weight: 600;\n line-height: 150%;\n letter-spacing: -0.1px;\n white-space: nowrap;\n flex-shrink: 0;\n}\n.al-badge-new {\n display: flex;\n width: 16px;\n height: 16px;\n justify-content: center;\n align-items: center;\n aspect-ratio: 1/1;\n border-radius: 12px;\n background: #7346f3;\n color: var(--al-badge-new-text);\n font-family:\n Pretendard,\n Inter,\n sans-serif;\n font-size: 9px;\n font-weight: 700;\n flex-shrink: 0;\n}\n");
|
|
242
|
+
|
|
243
|
+
// src/components/AppLauncherContent.tsx
|
|
244
|
+
var import_jsx_runtime4 = require("react/jsx-runtime");
|
|
245
|
+
function AppLauncherContent({
|
|
246
|
+
align = "end",
|
|
247
|
+
sideOffset = 12,
|
|
248
|
+
className
|
|
249
|
+
}) {
|
|
250
|
+
const { env, theme, isMobile, domain, onNavigate, close } = useAppLauncherContext();
|
|
251
|
+
const { data } = useGlobalMenu();
|
|
252
|
+
const items = React2.useMemo(
|
|
253
|
+
() => (data?.items ?? []).sort((a, b) => a.order - b.order),
|
|
254
|
+
[data]
|
|
255
|
+
);
|
|
256
|
+
const DRAG_THRESHOLD = 8;
|
|
257
|
+
const pointerStart = React2.useRef(null);
|
|
258
|
+
const handlePointerDown = React2.useCallback((e) => {
|
|
259
|
+
pointerStart.current = { x: e.clientX, y: e.clientY };
|
|
260
|
+
}, []);
|
|
261
|
+
const handleItemClick = React2.useCallback(
|
|
262
|
+
(e, item) => {
|
|
263
|
+
if (pointerStart.current) {
|
|
264
|
+
const dx = e.clientX - pointerStart.current.x;
|
|
265
|
+
const dy = e.clientY - pointerStart.current.y;
|
|
266
|
+
pointerStart.current = null;
|
|
267
|
+
if (Math.abs(dx) > DRAG_THRESHOLD || Math.abs(dy) > DRAG_THRESHOLD) {
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
const url = item.url[env] ?? item.url.production;
|
|
272
|
+
const isExternal = isExternalLink(url, domain);
|
|
273
|
+
if (isExternal) {
|
|
274
|
+
window.open(url, "_blank", "noopener,noreferrer");
|
|
275
|
+
} else {
|
|
276
|
+
const path = new URL(url).pathname;
|
|
277
|
+
if (onNavigate) {
|
|
278
|
+
onNavigate(path);
|
|
279
|
+
} else {
|
|
280
|
+
window.location.href = path;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
close();
|
|
284
|
+
},
|
|
285
|
+
[env, domain, onNavigate, close]
|
|
286
|
+
);
|
|
287
|
+
const currentPathname = typeof window !== "undefined" ? window.location.pathname : "";
|
|
288
|
+
const menuContent = /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "al-grid", "data-theme": theme, children: items.map((item) => {
|
|
289
|
+
const url = item.url[env] ?? item.url.production;
|
|
290
|
+
const isExternal = isExternalLink(url, domain);
|
|
291
|
+
let isActive = false;
|
|
292
|
+
if (!isExternal) {
|
|
293
|
+
try {
|
|
294
|
+
const urlPathname = new URL(url).pathname;
|
|
295
|
+
isActive = currentPathname === urlPathname || currentPathname.startsWith(urlPathname + "/");
|
|
296
|
+
} catch {
|
|
297
|
+
isActive = false;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
|
|
301
|
+
"button",
|
|
302
|
+
{
|
|
303
|
+
type: "button",
|
|
304
|
+
className: "al-grid-item",
|
|
305
|
+
"data-active": isActive || void 0,
|
|
306
|
+
onPointerDown: handlePointerDown,
|
|
307
|
+
onClick: (e) => handleItemClick(e, item),
|
|
308
|
+
children: [
|
|
309
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "al-grid-item-left", children: [
|
|
310
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "al-grid-item-icon", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
311
|
+
"img",
|
|
312
|
+
{
|
|
313
|
+
src: item.iconUrl,
|
|
314
|
+
alt: item.label,
|
|
315
|
+
width: 24,
|
|
316
|
+
height: 24,
|
|
317
|
+
loading: "lazy"
|
|
318
|
+
}
|
|
319
|
+
) }),
|
|
320
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "al-grid-item-label", children: item.label }),
|
|
321
|
+
item.isNew && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "al-badge-new", children: "N" })
|
|
322
|
+
] }),
|
|
323
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "al-grid-item-right", children: item.badge && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "al-badge", children: item.badge }) })
|
|
324
|
+
]
|
|
325
|
+
},
|
|
326
|
+
item.id
|
|
327
|
+
);
|
|
328
|
+
}) });
|
|
329
|
+
if (isMobile) {
|
|
330
|
+
return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_vaul3.Drawer.Portal, { children: [
|
|
331
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_vaul3.Drawer.Overlay, { className: "al-drawer-overlay" }),
|
|
332
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
|
|
333
|
+
import_vaul3.Drawer.Content,
|
|
334
|
+
{
|
|
335
|
+
className: cn("al-drawer-content", className),
|
|
336
|
+
"data-theme": theme,
|
|
337
|
+
children: [
|
|
338
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "al-drawer-header", children: [
|
|
339
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_vaul3.Drawer.Close, { className: "al-drawer-close", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(BackArrowIcon, {}) }),
|
|
340
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_vaul3.Drawer.Title, { className: "al-drawer-title", children: "CROSS Services" }),
|
|
341
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_vaul3.Drawer.Close, { className: "al-drawer-close", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(CloseIcon, {}) })
|
|
342
|
+
] }),
|
|
343
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "al-drawer-body", children: menuContent })
|
|
344
|
+
]
|
|
345
|
+
}
|
|
346
|
+
)
|
|
347
|
+
] });
|
|
348
|
+
}
|
|
349
|
+
return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Popover3.Portal, { children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
350
|
+
Popover3.Content,
|
|
351
|
+
{
|
|
352
|
+
align,
|
|
353
|
+
sideOffset,
|
|
354
|
+
className: cn("al-popover-content", className),
|
|
355
|
+
"data-theme": theme,
|
|
356
|
+
children: menuContent
|
|
357
|
+
}
|
|
358
|
+
) });
|
|
359
|
+
}
|
|
360
|
+
function isExternalLink(url, domain) {
|
|
361
|
+
try {
|
|
362
|
+
const linkHostname = new URL(url).hostname;
|
|
363
|
+
if (typeof window === "undefined") return true;
|
|
364
|
+
return linkHostname !== (domain ?? window.location.hostname);
|
|
365
|
+
} catch {
|
|
366
|
+
return false;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
function cn(...classes) {
|
|
370
|
+
return classes.filter(Boolean).join(" ");
|
|
371
|
+
}
|
|
372
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
373
|
+
0 && (module.exports = {
|
|
374
|
+
AppLauncher,
|
|
375
|
+
AppLauncherContent,
|
|
376
|
+
AppLauncherTrigger,
|
|
377
|
+
useGlobalMenu
|
|
378
|
+
});
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
import * as _tanstack_react_query from '@tanstack/react-query';
|
|
4
|
+
|
|
5
|
+
type Environment = "dev" | "stage" | "production";
|
|
6
|
+
interface GlobalMenuItemUrl {
|
|
7
|
+
dev: string;
|
|
8
|
+
stage: string;
|
|
9
|
+
production: string;
|
|
10
|
+
}
|
|
11
|
+
interface GlobalMenuItem {
|
|
12
|
+
id: string;
|
|
13
|
+
label: string;
|
|
14
|
+
description: string;
|
|
15
|
+
url: GlobalMenuItemUrl;
|
|
16
|
+
iconUrl: string;
|
|
17
|
+
order: number;
|
|
18
|
+
type: string;
|
|
19
|
+
badge: string | null;
|
|
20
|
+
isNew: boolean;
|
|
21
|
+
}
|
|
22
|
+
interface GlobalMenu {
|
|
23
|
+
version: string;
|
|
24
|
+
items: GlobalMenuItem[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface AppLauncherProps {
|
|
28
|
+
env?: Environment;
|
|
29
|
+
theme?: "dark" | "light";
|
|
30
|
+
mobileBreakpoint?: number;
|
|
31
|
+
domain?: string;
|
|
32
|
+
onNavigate?: (path: string) => void;
|
|
33
|
+
children: React.ReactNode;
|
|
34
|
+
}
|
|
35
|
+
declare function AppLauncher({ env, theme, mobileBreakpoint, domain, onNavigate, children, }: AppLauncherProps): react_jsx_runtime.JSX.Element;
|
|
36
|
+
|
|
37
|
+
interface AppLauncherTriggerProps {
|
|
38
|
+
asChild?: boolean;
|
|
39
|
+
children?: React.ReactNode;
|
|
40
|
+
}
|
|
41
|
+
declare function AppLauncherTrigger({ asChild, children, }: AppLauncherTriggerProps): react_jsx_runtime.JSX.Element;
|
|
42
|
+
|
|
43
|
+
interface AppLauncherContentProps {
|
|
44
|
+
align?: "start" | "center" | "end";
|
|
45
|
+
sideOffset?: number;
|
|
46
|
+
className?: string;
|
|
47
|
+
}
|
|
48
|
+
declare function AppLauncherContent({ align, sideOffset, className, }: AppLauncherContentProps): react_jsx_runtime.JSX.Element;
|
|
49
|
+
|
|
50
|
+
declare function useGlobalMenu(): _tanstack_react_query.UseQueryResult<GlobalMenu, Error>;
|
|
51
|
+
|
|
52
|
+
export { AppLauncher, AppLauncherContent, type AppLauncherContentProps, type AppLauncherProps, AppLauncherTrigger, type AppLauncherTriggerProps, type Environment, type GlobalMenu, type GlobalMenuItem, type GlobalMenuItemUrl, useGlobalMenu };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
import * as _tanstack_react_query from '@tanstack/react-query';
|
|
4
|
+
|
|
5
|
+
type Environment = "dev" | "stage" | "production";
|
|
6
|
+
interface GlobalMenuItemUrl {
|
|
7
|
+
dev: string;
|
|
8
|
+
stage: string;
|
|
9
|
+
production: string;
|
|
10
|
+
}
|
|
11
|
+
interface GlobalMenuItem {
|
|
12
|
+
id: string;
|
|
13
|
+
label: string;
|
|
14
|
+
description: string;
|
|
15
|
+
url: GlobalMenuItemUrl;
|
|
16
|
+
iconUrl: string;
|
|
17
|
+
order: number;
|
|
18
|
+
type: string;
|
|
19
|
+
badge: string | null;
|
|
20
|
+
isNew: boolean;
|
|
21
|
+
}
|
|
22
|
+
interface GlobalMenu {
|
|
23
|
+
version: string;
|
|
24
|
+
items: GlobalMenuItem[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface AppLauncherProps {
|
|
28
|
+
env?: Environment;
|
|
29
|
+
theme?: "dark" | "light";
|
|
30
|
+
mobileBreakpoint?: number;
|
|
31
|
+
domain?: string;
|
|
32
|
+
onNavigate?: (path: string) => void;
|
|
33
|
+
children: React.ReactNode;
|
|
34
|
+
}
|
|
35
|
+
declare function AppLauncher({ env, theme, mobileBreakpoint, domain, onNavigate, children, }: AppLauncherProps): react_jsx_runtime.JSX.Element;
|
|
36
|
+
|
|
37
|
+
interface AppLauncherTriggerProps {
|
|
38
|
+
asChild?: boolean;
|
|
39
|
+
children?: React.ReactNode;
|
|
40
|
+
}
|
|
41
|
+
declare function AppLauncherTrigger({ asChild, children, }: AppLauncherTriggerProps): react_jsx_runtime.JSX.Element;
|
|
42
|
+
|
|
43
|
+
interface AppLauncherContentProps {
|
|
44
|
+
align?: "start" | "center" | "end";
|
|
45
|
+
sideOffset?: number;
|
|
46
|
+
className?: string;
|
|
47
|
+
}
|
|
48
|
+
declare function AppLauncherContent({ align, sideOffset, className, }: AppLauncherContentProps): react_jsx_runtime.JSX.Element;
|
|
49
|
+
|
|
50
|
+
declare function useGlobalMenu(): _tanstack_react_query.UseQueryResult<GlobalMenu, Error>;
|
|
51
|
+
|
|
52
|
+
export { AppLauncher, AppLauncherContent, type AppLauncherContentProps, type AppLauncherProps, AppLauncherTrigger, type AppLauncherTriggerProps, type Environment, type GlobalMenu, type GlobalMenuItem, type GlobalMenuItemUrl, useGlobalMenu };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
// src/components/AppLauncher.tsx
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import * as Popover from "@radix-ui/react-popover";
|
|
4
|
+
import { Drawer } from "vaul";
|
|
5
|
+
|
|
6
|
+
// src/context.ts
|
|
7
|
+
import { createContext, useContext } from "react";
|
|
8
|
+
var AppLauncherContext = createContext(
|
|
9
|
+
null
|
|
10
|
+
);
|
|
11
|
+
function useAppLauncherContext() {
|
|
12
|
+
const ctx = useContext(AppLauncherContext);
|
|
13
|
+
if (!ctx) {
|
|
14
|
+
throw new Error(
|
|
15
|
+
"AppLauncher compound components must be used within <AppLauncher>"
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
return ctx;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// src/hooks/useMediaQuery.ts
|
|
22
|
+
import { useState, useEffect } from "react";
|
|
23
|
+
function useMediaQuery(query) {
|
|
24
|
+
const [matches, setMatches] = useState(false);
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
const mql = window.matchMedia(query);
|
|
27
|
+
setMatches(mql.matches);
|
|
28
|
+
const handler = (e) => setMatches(e.matches);
|
|
29
|
+
mql.addEventListener("change", handler);
|
|
30
|
+
return () => mql.removeEventListener("change", handler);
|
|
31
|
+
}, [query]);
|
|
32
|
+
return matches;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// src/components/AppLauncher.tsx
|
|
36
|
+
import { jsx } from "react/jsx-runtime";
|
|
37
|
+
function AppLauncher({
|
|
38
|
+
env = "production",
|
|
39
|
+
theme = "dark",
|
|
40
|
+
mobileBreakpoint = 768,
|
|
41
|
+
domain,
|
|
42
|
+
onNavigate,
|
|
43
|
+
children
|
|
44
|
+
}) {
|
|
45
|
+
const isMobile = useMediaQuery(`(max-width: ${mobileBreakpoint}px)`);
|
|
46
|
+
const [open, setOpen] = React.useState(false);
|
|
47
|
+
const close = React.useCallback(() => setOpen(false), []);
|
|
48
|
+
const contextValue = React.useMemo(
|
|
49
|
+
() => ({ env, theme, isMobile, open, domain, onNavigate, close }),
|
|
50
|
+
[env, theme, isMobile, open, domain, onNavigate, close]
|
|
51
|
+
);
|
|
52
|
+
return /* @__PURE__ */ jsx(AppLauncherContext.Provider, { value: contextValue, children: isMobile ? /* @__PURE__ */ jsx(
|
|
53
|
+
Drawer.Root,
|
|
54
|
+
{
|
|
55
|
+
direction: "right",
|
|
56
|
+
open,
|
|
57
|
+
onOpenChange: setOpen,
|
|
58
|
+
children
|
|
59
|
+
},
|
|
60
|
+
"drawer"
|
|
61
|
+
) : /* @__PURE__ */ jsx(Popover.Root, { open, onOpenChange: setOpen, children }, "popover") });
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// src/components/AppLauncherTrigger.tsx
|
|
65
|
+
import * as Popover2 from "@radix-ui/react-popover";
|
|
66
|
+
import { Drawer as Drawer2 } from "vaul";
|
|
67
|
+
|
|
68
|
+
// src/components/icons.tsx
|
|
69
|
+
import { jsx as jsx2, jsxs } from "react/jsx-runtime";
|
|
70
|
+
function DotsGridIcon({ size = 18 }) {
|
|
71
|
+
return /* @__PURE__ */ jsxs(
|
|
72
|
+
"svg",
|
|
73
|
+
{
|
|
74
|
+
className: "al-trigger-icon",
|
|
75
|
+
width: size,
|
|
76
|
+
height: size,
|
|
77
|
+
viewBox: "0 0 18 18",
|
|
78
|
+
fill: "none",
|
|
79
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
80
|
+
children: [
|
|
81
|
+
/* @__PURE__ */ jsx2("rect", { width: "4", height: "4", fill: "currentColor" }),
|
|
82
|
+
/* @__PURE__ */ jsx2("rect", { x: "7", width: "4", height: "4", fill: "currentColor" }),
|
|
83
|
+
/* @__PURE__ */ jsx2("rect", { x: "14", width: "4", height: "4", fill: "currentColor" }),
|
|
84
|
+
/* @__PURE__ */ jsx2("rect", { y: "7", width: "4", height: "4", fill: "currentColor" }),
|
|
85
|
+
/* @__PURE__ */ jsx2("rect", { x: "7", y: "7", width: "4", height: "4", fill: "currentColor" }),
|
|
86
|
+
/* @__PURE__ */ jsx2("rect", { x: "14", y: "7", width: "4", height: "4", fill: "currentColor" }),
|
|
87
|
+
/* @__PURE__ */ jsx2("rect", { y: "14", width: "4", height: "4", fill: "currentColor" }),
|
|
88
|
+
/* @__PURE__ */ jsx2("rect", { x: "7", y: "14", width: "4", height: "4", fill: "currentColor" }),
|
|
89
|
+
/* @__PURE__ */ jsx2("rect", { x: "14", y: "14", width: "4", height: "4", fill: "currentColor" })
|
|
90
|
+
]
|
|
91
|
+
}
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
function BackArrowIcon() {
|
|
95
|
+
return /* @__PURE__ */ jsx2(
|
|
96
|
+
"svg",
|
|
97
|
+
{
|
|
98
|
+
width: "24",
|
|
99
|
+
height: "24",
|
|
100
|
+
viewBox: "0 0 24 24",
|
|
101
|
+
fill: "none",
|
|
102
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
103
|
+
children: /* @__PURE__ */ jsx2(
|
|
104
|
+
"path",
|
|
105
|
+
{
|
|
106
|
+
d: "M12 19L5 12M5 12L12 5M5 12H19",
|
|
107
|
+
stroke: "currentColor",
|
|
108
|
+
strokeWidth: "1.5",
|
|
109
|
+
strokeLinecap: "round",
|
|
110
|
+
strokeLinejoin: "round"
|
|
111
|
+
}
|
|
112
|
+
)
|
|
113
|
+
}
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
function CloseIcon() {
|
|
117
|
+
return /* @__PURE__ */ jsx2(
|
|
118
|
+
"svg",
|
|
119
|
+
{
|
|
120
|
+
width: "20",
|
|
121
|
+
height: "20",
|
|
122
|
+
viewBox: "0 0 20 20",
|
|
123
|
+
fill: "none",
|
|
124
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
125
|
+
children: /* @__PURE__ */ jsx2(
|
|
126
|
+
"path",
|
|
127
|
+
{
|
|
128
|
+
d: "M15 5L5 15M5 5L15 15",
|
|
129
|
+
stroke: "currentColor",
|
|
130
|
+
strokeWidth: "1.5",
|
|
131
|
+
strokeLinecap: "round",
|
|
132
|
+
strokeLinejoin: "round"
|
|
133
|
+
}
|
|
134
|
+
)
|
|
135
|
+
}
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// src/components/AppLauncherTrigger.tsx
|
|
140
|
+
import { jsx as jsx3 } from "react/jsx-runtime";
|
|
141
|
+
function AppLauncherTrigger({
|
|
142
|
+
asChild,
|
|
143
|
+
children
|
|
144
|
+
}) {
|
|
145
|
+
const { isMobile, theme } = useAppLauncherContext();
|
|
146
|
+
const hasChildren = children != null;
|
|
147
|
+
const shouldAsChild = asChild ?? true;
|
|
148
|
+
const triggerContent = hasChildren ? children : /* @__PURE__ */ jsx3("button", { type: "button", className: "al-trigger-btn", "data-theme": theme, children: /* @__PURE__ */ jsx3(DotsGridIcon, { size: isMobile ? 12 : 18 }) });
|
|
149
|
+
if (isMobile) {
|
|
150
|
+
return /* @__PURE__ */ jsx3(Drawer2.Trigger, { asChild: shouldAsChild, children: triggerContent });
|
|
151
|
+
}
|
|
152
|
+
return /* @__PURE__ */ jsx3(Popover2.Trigger, { asChild: shouldAsChild, children: triggerContent });
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// src/components/AppLauncherContent.tsx
|
|
156
|
+
import * as React2 from "react";
|
|
157
|
+
import * as Popover3 from "@radix-ui/react-popover";
|
|
158
|
+
import { Drawer as Drawer3 } from "vaul";
|
|
159
|
+
|
|
160
|
+
// src/hooks/useGlobalMenu.ts
|
|
161
|
+
import { useQuery } from "@tanstack/react-query";
|
|
162
|
+
var GLOBAL_MENU_URL = "https://contents.crosstoken.io/frontend/common/app-launcher/app-launcher.json";
|
|
163
|
+
function useGlobalMenu() {
|
|
164
|
+
return useQuery({
|
|
165
|
+
queryKey: ["global-menu"],
|
|
166
|
+
queryFn: async () => {
|
|
167
|
+
const res = await fetch(GLOBAL_MENU_URL, { cache: "no-store" });
|
|
168
|
+
if (!res.ok) {
|
|
169
|
+
throw new Error(`Failed to fetch global menu: ${res.status}`);
|
|
170
|
+
}
|
|
171
|
+
return res.json();
|
|
172
|
+
},
|
|
173
|
+
staleTime: 0,
|
|
174
|
+
gcTime: 0,
|
|
175
|
+
refetchOnMount: true
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// #style-inject:#style-inject
|
|
180
|
+
function styleInject(css, { insertAt } = {}) {
|
|
181
|
+
if (!css || typeof document === "undefined") return;
|
|
182
|
+
const head = document.head || document.getElementsByTagName("head")[0];
|
|
183
|
+
const style = document.createElement("style");
|
|
184
|
+
style.type = "text/css";
|
|
185
|
+
if (insertAt === "top") {
|
|
186
|
+
if (head.firstChild) {
|
|
187
|
+
head.insertBefore(style, head.firstChild);
|
|
188
|
+
} else {
|
|
189
|
+
head.appendChild(style);
|
|
190
|
+
}
|
|
191
|
+
} else {
|
|
192
|
+
head.appendChild(style);
|
|
193
|
+
}
|
|
194
|
+
if (style.styleSheet) {
|
|
195
|
+
style.styleSheet.cssText = css;
|
|
196
|
+
} else {
|
|
197
|
+
style.appendChild(document.createTextNode(css));
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// src/styles.css
|
|
202
|
+
styleInject("[data-theme=dark] {\n --al-bg: #1e232e;\n --al-bg-hover: #252b39;\n --al-text: hsla(200, 19%, 94%, 1);\n --al-text-secondary: hsla(200, 10%, 70%, 1);\n --al-border: #3b4153;\n --al-shadow: 0 2px 8px -1px rgba(0, 0, 0, 0.15);\n --al-badge-bg: #123f3c;\n --al-badge-text: hsla(168, 85%, 47%, 1);\n --al-badge-new-text: #000;\n --al-drawer-overlay: rgba(0, 0, 0, 0.6);\n}\n[data-theme=light] {\n --al-bg: #fff;\n --al-bg-hover: #f3f6f8;\n --al-text: hsla(221, 21%, 15%, 1);\n --al-text-secondary: hsla(220, 10%, 40%, 1);\n --al-border: #ecf0f2;\n --al-shadow: 0 2px 8px -1px rgba(0, 0, 0, 0.15);\n --al-badge-bg: #cdf4ed;\n --al-badge-text: hsla(170, 82%, 32%, 1);\n --al-badge-new-text: #fff;\n --al-drawer-overlay: rgba(0, 0, 0, 0.4);\n}\n.al-trigger-btn {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n padding: 8px;\n border: none;\n background: transparent;\n border-radius: 8px;\n cursor: pointer;\n transition: background-color 0.15s ease;\n}\n.al-trigger-btn:hover {\n background: rgba(128, 128, 128, 0.1);\n}\n.al-trigger-btn[data-theme=dark] {\n color: hsla(200, 19%, 94%, 1);\n}\n.al-trigger-btn[data-theme=light] {\n color: hsla(221, 21%, 15%, 1);\n}\n[data-state=open] > .al-trigger-btn[data-theme=dark],\n.al-trigger-btn[data-state=open][data-theme=dark] {\n border-radius: 14px;\n background: #161b22;\n}\n[data-state=open] > .al-trigger-btn[data-theme=light],\n.al-trigger-btn[data-state=open][data-theme=light] {\n border-radius: 14px;\n background: #ecf0f2;\n}\n.al-popover-content {\n z-index: 50;\n border-radius: 12px;\n background: var(--al-bg);\n border: 1px solid var(--al-border);\n box-shadow: var(--al-shadow);\n animation: al-fade-in 0.15s ease-out;\n max-height: 80vh;\n overflow-x: hidden;\n overflow-y: auto;\n}\n@keyframes al-fade-in {\n from {\n opacity: 0;\n transform: translateY(-4px);\n }\n to {\n opacity: 1;\n transform: translateY(0);\n }\n}\n.al-drawer-overlay {\n position: fixed;\n inset: 0;\n z-index: 49;\n background: var(--al-drawer-overlay);\n}\n.al-drawer-content {\n position: fixed;\n top: 0;\n right: 0;\n bottom: 0;\n z-index: 50;\n width: 360px;\n max-width: 100vw;\n background: var(--al-bg);\n overflow-x: hidden;\n overflow-y: hidden;\n display: flex;\n flex-direction: column;\n box-sizing: border-box;\n}\n.al-drawer-body {\n flex: 1;\n overflow-y: auto;\n overscroll-behavior-y: contain;\n min-height: 0;\n}\n.al-drawer-content .al-grid {\n grid-template-columns: 1fr;\n max-width: 100%;\n overflow: hidden;\n flex-shrink: 0;\n gap: 4px;\n}\n.al-drawer-content .al-grid-item {\n max-width: 100%;\n overflow: hidden;\n}\n.al-drawer-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 16px 20px;\n flex-shrink: 0;\n}\n.al-drawer-title {\n font-family: Inter, sans-serif;\n font-size: 18px;\n font-weight: 700;\n color: var(--al-text);\n margin: 0;\n}\n.al-drawer-close {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n padding: 4px;\n border: none;\n background: transparent;\n border-radius: 4px;\n cursor: pointer;\n color: var(--al-text);\n transition: opacity 0.15s ease;\n}\n.al-drawer-close:hover {\n opacity: 0.7;\n}\n.al-grid {\n display: grid;\n grid-template-columns: repeat(2, 1fr);\n gap: 4px;\n padding: 8px;\n}\n.al-grid-item {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 10px;\n padding: 16px 12px;\n border: none;\n background: transparent;\n border-radius: 8px;\n cursor: pointer;\n transition: background-color 0.15s ease;\n color: var(--al-text);\n text-align: left;\n font-family: Inter, sans-serif;\n flex-shrink: 0;\n}\n.al-grid-item:hover,\n.al-grid-item[data-active=true] {\n background: var(--al-bg-hover);\n}\n.al-grid-item-left {\n display: flex;\n align-items: center;\n gap: 8px;\n min-width: 0;\n overflow: hidden;\n}\n.al-grid-item-right {\n display: flex;\n align-items: center;\n gap: 6px;\n flex-shrink: 0;\n}\n.al-grid-item-icon {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 24px;\n height: 24px;\n flex-shrink: 0;\n}\n.al-grid-item-icon img {\n width: 100%;\n height: 100%;\n object-fit: contain;\n}\n.al-grid-item-label {\n font-family: Inter, sans-serif;\n font-size: 16px;\n font-weight: 500;\n line-height: 150%;\n letter-spacing: -0.16px;\n color: var(--al-text);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n.al-badge {\n display: flex;\n padding: 1px 4px 2px 4px;\n justify-content: center;\n align-items: center;\n gap: 2px;\n border-radius: 3px;\n background: var(--al-badge-bg);\n color: var(--al-badge-text);\n font-family:\n Pretendard,\n Inter,\n sans-serif;\n font-size: 10px;\n font-weight: 600;\n line-height: 150%;\n letter-spacing: -0.1px;\n white-space: nowrap;\n flex-shrink: 0;\n}\n.al-badge-new {\n display: flex;\n width: 16px;\n height: 16px;\n justify-content: center;\n align-items: center;\n aspect-ratio: 1/1;\n border-radius: 12px;\n background: #7346f3;\n color: var(--al-badge-new-text);\n font-family:\n Pretendard,\n Inter,\n sans-serif;\n font-size: 9px;\n font-weight: 700;\n flex-shrink: 0;\n}\n");
|
|
203
|
+
|
|
204
|
+
// src/components/AppLauncherContent.tsx
|
|
205
|
+
import { jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
206
|
+
function AppLauncherContent({
|
|
207
|
+
align = "end",
|
|
208
|
+
sideOffset = 12,
|
|
209
|
+
className
|
|
210
|
+
}) {
|
|
211
|
+
const { env, theme, isMobile, domain, onNavigate, close } = useAppLauncherContext();
|
|
212
|
+
const { data } = useGlobalMenu();
|
|
213
|
+
const items = React2.useMemo(
|
|
214
|
+
() => (data?.items ?? []).sort((a, b) => a.order - b.order),
|
|
215
|
+
[data]
|
|
216
|
+
);
|
|
217
|
+
const DRAG_THRESHOLD = 8;
|
|
218
|
+
const pointerStart = React2.useRef(null);
|
|
219
|
+
const handlePointerDown = React2.useCallback((e) => {
|
|
220
|
+
pointerStart.current = { x: e.clientX, y: e.clientY };
|
|
221
|
+
}, []);
|
|
222
|
+
const handleItemClick = React2.useCallback(
|
|
223
|
+
(e, item) => {
|
|
224
|
+
if (pointerStart.current) {
|
|
225
|
+
const dx = e.clientX - pointerStart.current.x;
|
|
226
|
+
const dy = e.clientY - pointerStart.current.y;
|
|
227
|
+
pointerStart.current = null;
|
|
228
|
+
if (Math.abs(dx) > DRAG_THRESHOLD || Math.abs(dy) > DRAG_THRESHOLD) {
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
const url = item.url[env] ?? item.url.production;
|
|
233
|
+
const isExternal = isExternalLink(url, domain);
|
|
234
|
+
if (isExternal) {
|
|
235
|
+
window.open(url, "_blank", "noopener,noreferrer");
|
|
236
|
+
} else {
|
|
237
|
+
const path = new URL(url).pathname;
|
|
238
|
+
if (onNavigate) {
|
|
239
|
+
onNavigate(path);
|
|
240
|
+
} else {
|
|
241
|
+
window.location.href = path;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
close();
|
|
245
|
+
},
|
|
246
|
+
[env, domain, onNavigate, close]
|
|
247
|
+
);
|
|
248
|
+
const currentPathname = typeof window !== "undefined" ? window.location.pathname : "";
|
|
249
|
+
const menuContent = /* @__PURE__ */ jsx4("div", { className: "al-grid", "data-theme": theme, children: items.map((item) => {
|
|
250
|
+
const url = item.url[env] ?? item.url.production;
|
|
251
|
+
const isExternal = isExternalLink(url, domain);
|
|
252
|
+
let isActive = false;
|
|
253
|
+
if (!isExternal) {
|
|
254
|
+
try {
|
|
255
|
+
const urlPathname = new URL(url).pathname;
|
|
256
|
+
isActive = currentPathname === urlPathname || currentPathname.startsWith(urlPathname + "/");
|
|
257
|
+
} catch {
|
|
258
|
+
isActive = false;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
return /* @__PURE__ */ jsxs2(
|
|
262
|
+
"button",
|
|
263
|
+
{
|
|
264
|
+
type: "button",
|
|
265
|
+
className: "al-grid-item",
|
|
266
|
+
"data-active": isActive || void 0,
|
|
267
|
+
onPointerDown: handlePointerDown,
|
|
268
|
+
onClick: (e) => handleItemClick(e, item),
|
|
269
|
+
children: [
|
|
270
|
+
/* @__PURE__ */ jsxs2("div", { className: "al-grid-item-left", children: [
|
|
271
|
+
/* @__PURE__ */ jsx4("div", { className: "al-grid-item-icon", children: /* @__PURE__ */ jsx4(
|
|
272
|
+
"img",
|
|
273
|
+
{
|
|
274
|
+
src: item.iconUrl,
|
|
275
|
+
alt: item.label,
|
|
276
|
+
width: 24,
|
|
277
|
+
height: 24,
|
|
278
|
+
loading: "lazy"
|
|
279
|
+
}
|
|
280
|
+
) }),
|
|
281
|
+
/* @__PURE__ */ jsx4("span", { className: "al-grid-item-label", children: item.label }),
|
|
282
|
+
item.isNew && /* @__PURE__ */ jsx4("span", { className: "al-badge-new", children: "N" })
|
|
283
|
+
] }),
|
|
284
|
+
/* @__PURE__ */ jsx4("div", { className: "al-grid-item-right", children: item.badge && /* @__PURE__ */ jsx4("span", { className: "al-badge", children: item.badge }) })
|
|
285
|
+
]
|
|
286
|
+
},
|
|
287
|
+
item.id
|
|
288
|
+
);
|
|
289
|
+
}) });
|
|
290
|
+
if (isMobile) {
|
|
291
|
+
return /* @__PURE__ */ jsxs2(Drawer3.Portal, { children: [
|
|
292
|
+
/* @__PURE__ */ jsx4(Drawer3.Overlay, { className: "al-drawer-overlay" }),
|
|
293
|
+
/* @__PURE__ */ jsxs2(
|
|
294
|
+
Drawer3.Content,
|
|
295
|
+
{
|
|
296
|
+
className: cn("al-drawer-content", className),
|
|
297
|
+
"data-theme": theme,
|
|
298
|
+
children: [
|
|
299
|
+
/* @__PURE__ */ jsxs2("div", { className: "al-drawer-header", children: [
|
|
300
|
+
/* @__PURE__ */ jsx4(Drawer3.Close, { className: "al-drawer-close", children: /* @__PURE__ */ jsx4(BackArrowIcon, {}) }),
|
|
301
|
+
/* @__PURE__ */ jsx4(Drawer3.Title, { className: "al-drawer-title", children: "CROSS Services" }),
|
|
302
|
+
/* @__PURE__ */ jsx4(Drawer3.Close, { className: "al-drawer-close", children: /* @__PURE__ */ jsx4(CloseIcon, {}) })
|
|
303
|
+
] }),
|
|
304
|
+
/* @__PURE__ */ jsx4("div", { className: "al-drawer-body", children: menuContent })
|
|
305
|
+
]
|
|
306
|
+
}
|
|
307
|
+
)
|
|
308
|
+
] });
|
|
309
|
+
}
|
|
310
|
+
return /* @__PURE__ */ jsx4(Popover3.Portal, { children: /* @__PURE__ */ jsx4(
|
|
311
|
+
Popover3.Content,
|
|
312
|
+
{
|
|
313
|
+
align,
|
|
314
|
+
sideOffset,
|
|
315
|
+
className: cn("al-popover-content", className),
|
|
316
|
+
"data-theme": theme,
|
|
317
|
+
children: menuContent
|
|
318
|
+
}
|
|
319
|
+
) });
|
|
320
|
+
}
|
|
321
|
+
function isExternalLink(url, domain) {
|
|
322
|
+
try {
|
|
323
|
+
const linkHostname = new URL(url).hostname;
|
|
324
|
+
if (typeof window === "undefined") return true;
|
|
325
|
+
return linkHostname !== (domain ?? window.location.hostname);
|
|
326
|
+
} catch {
|
|
327
|
+
return false;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
function cn(...classes) {
|
|
331
|
+
return classes.filter(Boolean).join(" ");
|
|
332
|
+
}
|
|
333
|
+
export {
|
|
334
|
+
AppLauncher,
|
|
335
|
+
AppLauncherContent,
|
|
336
|
+
AppLauncherTrigger,
|
|
337
|
+
useGlobalMenu
|
|
338
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "cross-app-launcher",
|
|
3
|
+
"version": "0.0.0-beta.1",
|
|
4
|
+
"description": "Unified app launcher menu component for CROSS ecosystem services",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.cjs",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"import": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"default": "./dist/index.js"
|
|
14
|
+
},
|
|
15
|
+
"require": {
|
|
16
|
+
"types": "./dist/index.d.cts",
|
|
17
|
+
"default": "./dist/index.cjs"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"dist"
|
|
23
|
+
],
|
|
24
|
+
"scripts": {
|
|
25
|
+
"build": "tsup",
|
|
26
|
+
"dev": "tsup --watch",
|
|
27
|
+
"typecheck": "tsc --noEmit"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@radix-ui/react-popover": "^1.1.6",
|
|
31
|
+
"vaul": "^1.1.2"
|
|
32
|
+
},
|
|
33
|
+
"peerDependencies": {
|
|
34
|
+
"@tanstack/react-query": ">=5",
|
|
35
|
+
"react": ">=18",
|
|
36
|
+
"react-dom": ">=18"
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"@tanstack/react-query": "^5.64.0",
|
|
40
|
+
"@types/react": "^19.0.0",
|
|
41
|
+
"@types/react-dom": "^19.0.0",
|
|
42
|
+
"react": "^19.0.0",
|
|
43
|
+
"react-dom": "^19.0.0",
|
|
44
|
+
"tsup": "^8.4.0",
|
|
45
|
+
"typescript": "^5.7.0"
|
|
46
|
+
},
|
|
47
|
+
"sideEffects": [
|
|
48
|
+
"**/*.css"
|
|
49
|
+
],
|
|
50
|
+
"publishConfig": {
|
|
51
|
+
"registry": "https://registry.npmjs.org/",
|
|
52
|
+
"access": "public"
|
|
53
|
+
},
|
|
54
|
+
"repository": {
|
|
55
|
+
"type": "git",
|
|
56
|
+
"url": "https://github.com/to-nexus/app-launcher-frontend.git"
|
|
57
|
+
},
|
|
58
|
+
"license": "MIT"
|
|
59
|
+
}
|