@nexus-cross/design-system 2.0.3 → 2.0.4
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/claude-rules/nexus/CLAUDE.md +109 -0
- package/cursor-rules/CLAUDE.md +5 -0
- package/cursor-rules/nexus-project-setup.mdc +106 -6
- package/cursor-rules/nexus-ui-api.mdc +60 -3
- package/dist/chunks/{chunk-3VFBPFZF.mjs → chunk-7WWQ5DS3.mjs} +10 -5
- package/dist/chunks/{chunk-HHXDOKXY.js → chunk-F24AY3HI.js} +2 -2
- package/dist/chunks/{chunk-6H7V2I3X.mjs → chunk-FY2N42XN.mjs} +6 -1
- package/dist/chunks/{chunk-XEHFB62A.js → chunk-NRO7I4EI.js} +2 -6
- package/dist/chunks/{chunk-U53UA76K.js → chunk-R744EATX.js} +26 -21
- package/dist/chunks/{chunk-HUPAHDJ7.js → chunk-TAHDSSA6.js} +6 -0
- package/dist/chunks/{chunk-YEWKPWK3.mjs → chunk-U6OEUBIF.mjs} +2 -6
- package/dist/chunks/{chunk-U56AGSLE.mjs → chunk-WBCXHGRL.mjs} +2 -2
- package/dist/hooks/useCheckDevice.d.ts.map +1 -1
- package/dist/hooks/useCheckDevice.js +2 -2
- package/dist/hooks/useCheckDevice.mjs +1 -1
- package/dist/hooks/useDraggableBottomSheet.js +2 -2
- package/dist/hooks/useDraggableBottomSheet.mjs +1 -1
- package/dist/hooks/useModal.js +2 -2
- package/dist/hooks/useModal.mjs +1 -1
- package/dist/index.js +16 -16
- package/dist/index.mjs +4 -4
- package/dist/modal/components/ModalContainer.d.ts.map +1 -1
- package/dist/modal/components/ModalTemplate.d.ts +1 -0
- package/dist/modal/components/ModalTemplate.d.ts.map +1 -1
- package/dist/modal/constants.d.ts +14 -0
- package/dist/modal/constants.d.ts.map +1 -1
- package/dist/modal/index.js +14 -14
- package/dist/modal/index.mjs +4 -4
- package/dist/schemas/_all.json +9 -4
- package/dist/schemas/modal.d.ts +3 -0
- package/dist/schemas/modal.d.ts.map +1 -1
- package/dist/schemas/modalTemplate.json +9 -4
- package/dist/schemas.js +17 -4
- package/dist/schemas.mjs +17 -4
- package/dist/tokens/company.css +1 -1
- package/dist/tokens/css.css +1 -1
- package/dist/tokens-domains/gamehub-vars.css +122 -0
- package/dist/tokens-domains/gamehub.css +121 -0
- package/dist/tokens-domains/prediction-vars.css +1 -1
- package/dist/tokens-domains/prediction.css +1 -1
- package/package.json +4 -5
|
@@ -32,6 +32,11 @@
|
|
|
32
32
|
|
|
33
33
|
5. **새 UI 작업 전, 항상 컴포넌트 매핑부터.** native HTML이나 Tailwind 프리미티브로 구현하기 전, `ui-components.md`에 동일 기능이 있는지 먼저 확인.
|
|
34
34
|
|
|
35
|
+
6. **모달 콘텐츠 컴포넌트 작성 시:**
|
|
36
|
+
- props 타입은 **반드시 `ModalPropsType<T>` 를 확장**할 것 (`close`, `resolve`, `className`, `layout` 시그니처가 시스템에서 자동 주입됨)
|
|
37
|
+
- `<ModalTemplate>` 에는 시스템 주입 **`className` 과 `layout` 을 그대로 전달**할 것 (둘 중 하나라도 누락하면 wrap 스타일 / 모바일 bottom-sheet 자동 전환이 동작하지 않음)
|
|
38
|
+
- 자세한 패턴은 아래 "Modal Writing Rules" 섹션 참조
|
|
39
|
+
|
|
35
40
|
---
|
|
36
41
|
|
|
37
42
|
## Quick Install (단일 패키지)
|
|
@@ -167,6 +172,110 @@ export default function App() {
|
|
|
167
172
|
|
|
168
173
|
---
|
|
169
174
|
|
|
175
|
+
## Modal Writing Rules
|
|
176
|
+
|
|
177
|
+
모달 콘텐츠 컴포넌트는 다음 3가지 규칙을 **반드시** 지켜야 합니다.
|
|
178
|
+
|
|
179
|
+
### Rule 1. props 타입은 반드시 `ModalPropsType<T>` 를 확장하라
|
|
180
|
+
|
|
181
|
+
`modal()` / `useModal()` 로 모달이 mount 될 때 시스템이 다음 4개 prop 을 자동 주입합니다.
|
|
182
|
+
|
|
183
|
+
| 주입 prop | 타입 | 의미 |
|
|
184
|
+
|---|---|---|
|
|
185
|
+
| `close` | `(isAnimation?: boolean) => void` | 모달을 닫음 |
|
|
186
|
+
| `resolve` | `(value: T, useClose?: boolean) => void` | 호출자에게 결과 전달 (T 는 사용자가 정함) |
|
|
187
|
+
| `className` | `string` | 모달 wrap layer (full-screen 중앙 정렬 컨테이너) 에 들어가야 하는 시스템 클래스 |
|
|
188
|
+
| `layout` | `ModalLayout` | 시스템이 결정한 layout. 모바일 viewport 에서는 자동으로 `bottom-sheet` 등으로 변환됨 |
|
|
189
|
+
|
|
190
|
+
이 4개 prop 을 직접 손으로 타이핑하지 말고 **`ModalPropsType<T>` 를 확장하세요.** 손으로 정의하면 시스템 시그니처 변경 시 모달이 동작하지 않게 됩니다.
|
|
191
|
+
|
|
192
|
+
```tsx
|
|
193
|
+
// CORRECT — ModalPropsType 확장
|
|
194
|
+
import type { ModalPropsType } from '@nexus-cross/design-system';
|
|
195
|
+
|
|
196
|
+
// ModalPropsType<T> 의 T 는 resolve(value) 의 value 타입
|
|
197
|
+
type Props = ModalPropsType<boolean> & {
|
|
198
|
+
title: string;
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
function ConfirmModal({ className, layout, close, resolve, title }: Props) {
|
|
202
|
+
// ...
|
|
203
|
+
}
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
```tsx
|
|
207
|
+
// WRONG — 시스템 주입 prop 을 직접 손으로 정의
|
|
208
|
+
function ConfirmModal({
|
|
209
|
+
close,
|
|
210
|
+
resolve,
|
|
211
|
+
title,
|
|
212
|
+
}: {
|
|
213
|
+
close: () => void;
|
|
214
|
+
resolve: (v: boolean) => void;
|
|
215
|
+
title: string;
|
|
216
|
+
}) {
|
|
217
|
+
// className, layout 누락 → wrap 스타일 / 모바일 bottom-sheet 자동 전환 동작 X
|
|
218
|
+
}
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
### Rule 2. `<ModalTemplate>` 에는 시스템 주입 `className` 과 `layout` 을 그대로 전달하라
|
|
222
|
+
|
|
223
|
+
`className` 은 모달 **wrap layer** (full-screen `position: fixed; inset: 0` 중앙 정렬 컨테이너) 에 적용되는 시스템 클래스, `layout` 은 layout 별 분기(애니메이션, bottom-sheet 드래그, slide-left/right 등) 의 단일 source of truth 입니다. **둘 중 하나라도 누락하면 위치, dim, 애니메이션, 모바일 bottom-sheet 동작이 깨집니다.**
|
|
224
|
+
|
|
225
|
+
```tsx
|
|
226
|
+
// CORRECT — className / layout 그대로 통과
|
|
227
|
+
function ConfirmModal({ className, layout, close, resolve, title }: Props) {
|
|
228
|
+
return (
|
|
229
|
+
<ModalTemplate
|
|
230
|
+
className={className}
|
|
231
|
+
layout={layout}
|
|
232
|
+
title={title}
|
|
233
|
+
close={close}
|
|
234
|
+
footer={<Button onClick={() => resolve(true)}>확인</Button>}
|
|
235
|
+
>
|
|
236
|
+
<p className="text-text-secondary">정말 진행하시겠습니까?</p>
|
|
237
|
+
</ModalTemplate>
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
```tsx
|
|
243
|
+
// WRONG — 시스템 className / layout 누락
|
|
244
|
+
function ConfirmModal({ close, resolve, title }: Props) {
|
|
245
|
+
return (
|
|
246
|
+
// className, layout 안 넘김 → wrap 스타일 유실, 모바일 bottom-sheet 미적용
|
|
247
|
+
<ModalTemplate title={title} close={close}>
|
|
248
|
+
...
|
|
249
|
+
</ModalTemplate>
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// WRONG — 시스템 className 을 자체 className 으로 덮어씀
|
|
254
|
+
function BadModal({ className, layout, close }: Props) {
|
|
255
|
+
return (
|
|
256
|
+
// className 자리에 자체 클래스만 넣음 → 시스템 className 유실
|
|
257
|
+
<ModalTemplate className="bg-bg-default" layout={layout} close={close} title="…">
|
|
258
|
+
...
|
|
259
|
+
</ModalTemplate>
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
> 자체 클래스를 wrap 에 추가하고 싶다면 `cn(className, '내 클래스')` 형태로 결합. **wrap 에는 sizing utility (`w-*`, `max-w-*`, `h-*`) 금지** — sizing 은 `innerClassName` 으로.
|
|
265
|
+
|
|
266
|
+
### Rule 3. 모달 wrapper 는 직접 만들지 말 것
|
|
267
|
+
|
|
268
|
+
모달 콘텐츠는 **반드시 `ModalTemplate` 안에** 들어가야 합니다. 일반 `<div>` 로 만들면 focus trap / ESC handling / scroll lock / 애니메이션 / dim / a11y 가 모두 사라집니다.
|
|
269
|
+
|
|
270
|
+
### Self-check (모달 작성 후 출력 직전 검증)
|
|
271
|
+
|
|
272
|
+
- [ ] props 타입이 `ModalPropsType<T>` 를 확장하고 있는가?
|
|
273
|
+
- [ ] `<ModalTemplate>` 에 `className={className}` 와 `layout={layout}` 을 그대로 넘겼는가?
|
|
274
|
+
- [ ] `<ModalTemplate>` 외부에 별도 wrapper `<div>` 를 두지 않았는가?
|
|
275
|
+
- [ ] 자체 wrap 클래스를 추가했다면 `cn(className, '...')` 으로 시스템 className 을 보존했는가?
|
|
276
|
+
|
|
277
|
+
---
|
|
278
|
+
|
|
170
279
|
## Token Path Convention
|
|
171
280
|
|
|
172
281
|
모든 디자인 토큰은 5-segment 형식: `color.semantic.{namespace}.{slot}.{state}`
|
package/cursor-rules/CLAUDE.md
CHANGED
|
@@ -25,6 +25,11 @@
|
|
|
25
25
|
|
|
26
26
|
4. **`className` 오버라이드 시 `!important` 금지.** `cn()` 유틸이 프리픽스 충돌 자동 해소.
|
|
27
27
|
|
|
28
|
+
5. **모달 콘텐츠 컴포넌트 작성 시 두 가지 필수.**
|
|
29
|
+
- props 타입은 **반드시 `ModalPropsType<T>` 를 확장** (`close`, `resolve`, `className`, `layout` 시그니처가 시스템에서 자동 주입)
|
|
30
|
+
- `<ModalTemplate>` 에 시스템 주입 **`className` 과 `layout` 을 그대로 전달** (둘 중 하나라도 누락하면 wrap 스타일 / 모바일 bottom-sheet 자동 전환 동작 X)
|
|
31
|
+
- 자세한 패턴: `.cursor/rules/nexus-project-setup.mdc` 의 "Modal Writing Rules" 섹션
|
|
32
|
+
|
|
28
33
|
## 설치 (단일 패키지)
|
|
29
34
|
|
|
30
35
|
v2.0부터 `@nexus-cross/design-system` 1개 install로 토큰까지 모두 사용 가능합니다.
|
|
@@ -292,27 +292,120 @@ import { getPredictionTheme } from '@nexus-cross/design-system/tokens-domains';
|
|
|
292
292
|
|
|
293
293
|
## Modal Writing Rules
|
|
294
294
|
|
|
295
|
-
|
|
295
|
+
모달 콘텐츠 컴포넌트는 다음 3가지 규칙을 **반드시** 지켜야 합니다.
|
|
296
|
+
|
|
297
|
+
### Rule 1. props 타입은 반드시 `ModalPropsType<T>` 를 확장하라
|
|
298
|
+
|
|
299
|
+
`modal()` / `useModal()` 로 모달이 mount 될 때 시스템이 다음 4개 prop 을 자동 주입합니다.
|
|
300
|
+
|
|
301
|
+
| 주입 prop | 타입 | 의미 |
|
|
302
|
+
|---|---|---|
|
|
303
|
+
| `close` | `(isAnimation?: boolean) => void` | 모달을 닫음 |
|
|
304
|
+
| `resolve` | `(value: T, useClose?: boolean) => void` | 호출자에게 결과 전달 (T 는 사용자가 정함) |
|
|
305
|
+
| `className` | `string` | 모달 wrap layer (full-screen 중앙 정렬 컨테이너) 에 들어가야 하는 시스템 클래스 |
|
|
306
|
+
| `layout` | `ModalLayout` | 시스템이 결정한 layout. 모바일 viewport 에서는 자동으로 `bottom-sheet` 등으로 변환됨 |
|
|
307
|
+
|
|
308
|
+
이 4개 prop 을 직접 손으로 타이핑하지 말고 **`ModalPropsType<T>` 를 확장하세요.** 손으로 정의하면 시스템 주입 시그니처가 바뀔 때 모달이 동작하지 않게 됩니다.
|
|
296
309
|
|
|
297
310
|
```tsx
|
|
298
|
-
// CORRECT
|
|
311
|
+
// CORRECT — ModalPropsType 확장
|
|
312
|
+
import type { ModalPropsType } from '@nexus-cross/design-system';
|
|
313
|
+
|
|
314
|
+
// ModalPropsType<T> 의 T 는 resolve(value) 의 value 타입
|
|
315
|
+
type Props = ModalPropsType<boolean> & {
|
|
316
|
+
title: string;
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
function ConfirmModal({ className, layout, close, resolve, title }: Props) {
|
|
320
|
+
// ...
|
|
321
|
+
}
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
```tsx
|
|
325
|
+
// WRONG — 시스템 주입 prop 을 직접 손으로 정의
|
|
326
|
+
function ConfirmModal({
|
|
327
|
+
close,
|
|
328
|
+
resolve,
|
|
329
|
+
title,
|
|
330
|
+
}: {
|
|
331
|
+
close: () => void;
|
|
332
|
+
resolve: (v: boolean) => void;
|
|
333
|
+
title: string;
|
|
334
|
+
}) {
|
|
335
|
+
// className, layout 누락 → wrap 스타일 / 모바일 bottom-sheet 자동 전환 동작 X
|
|
336
|
+
}
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
### Rule 2. `<ModalTemplate>` 에는 시스템 주입 `className` 과 `layout` 을 그대로 전달하라
|
|
340
|
+
|
|
341
|
+
`className` 은 모달 **wrap layer** (full-screen `position: fixed; inset: 0` 중앙 정렬 컨테이너) 에 적용되는 시스템 클래스, `layout` 은 layout 별 분기(애니메이션, bottom-sheet 드래그, slide-left/right 등) 의 단일 source of truth 입니다. **둘 중 하나라도 누락하면 위치, dim, 애니메이션, 모바일 bottom-sheet 동작이 깨집니다.**
|
|
342
|
+
|
|
343
|
+
```tsx
|
|
344
|
+
// CORRECT — className / layout 그대로 통과
|
|
345
|
+
function ConfirmModal({ className, layout, close, resolve, title }: Props) {
|
|
346
|
+
return (
|
|
347
|
+
<ModalTemplate
|
|
348
|
+
className={className}
|
|
349
|
+
layout={layout}
|
|
350
|
+
title={title}
|
|
351
|
+
close={close}
|
|
352
|
+
footer={<Button onClick={() => resolve(true)}>확인</Button>}
|
|
353
|
+
>
|
|
354
|
+
<p className="text-text-secondary">정말 진행하시겠습니까?</p>
|
|
355
|
+
</ModalTemplate>
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
```tsx
|
|
361
|
+
// WRONG — 시스템 className / layout 누락
|
|
362
|
+
function ConfirmModal({ close, resolve, title }: Props) {
|
|
363
|
+
return (
|
|
364
|
+
// className, layout 안 넘김 → wrap 스타일 유실, 모바일 bottom-sheet 미적용
|
|
365
|
+
<ModalTemplate title={title} close={close}>
|
|
366
|
+
...
|
|
367
|
+
</ModalTemplate>
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// WRONG — 시스템 className 을 자체 className 으로 덮어씀
|
|
372
|
+
function BadModal({ className, layout, close }: Props) {
|
|
373
|
+
return (
|
|
374
|
+
// className 자리에 자체 클래스만 넣음 → 시스템 className 유실
|
|
375
|
+
<ModalTemplate className="bg-bg-default" layout={layout} close={close} title="…">
|
|
376
|
+
...
|
|
377
|
+
</ModalTemplate>
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
> 자체 클래스를 wrap 에 추가하고 싶다면 `cn(className, '내 클래스')` 형태로 결합. **wrap 에는 sizing utility (`w-*`, `max-w-*`, `h-*`) 금지** — sizing 은 `innerClassName` 으로.
|
|
383
|
+
|
|
384
|
+
### Rule 3. 모달 wrapper 는 직접 만들지 말 것
|
|
385
|
+
|
|
386
|
+
모달 콘텐츠는 **반드시 `ModalTemplate` 안에** 들어가야 합니다. 일반 `<div>` 로 만들면 focus trap / ESC handling / scroll lock / 애니메이션 / dim / a11y 가 모두 사라집니다.
|
|
387
|
+
|
|
388
|
+
```tsx
|
|
389
|
+
// CORRECT
|
|
299
390
|
import { ModalTemplate } from '@nexus-cross/design-system/modal';
|
|
391
|
+
import type { ModalPropsType } from '@nexus-cross/design-system';
|
|
392
|
+
|
|
393
|
+
type Props = ModalPropsType<{ confirmed: boolean }>;
|
|
300
394
|
|
|
301
|
-
function MyModal({
|
|
395
|
+
function MyModal({ className, layout, close, resolve }: Props) {
|
|
302
396
|
return (
|
|
303
|
-
<ModalTemplate title="Title" close={close}>
|
|
397
|
+
<ModalTemplate className={className} layout={layout} title="Title" close={close}>
|
|
304
398
|
<p className="text-text-secondary">Content</p>
|
|
305
399
|
<Button onClick={() => resolve({ confirmed: true })}>Confirm</Button>
|
|
306
400
|
</ModalTemplate>
|
|
307
401
|
);
|
|
308
402
|
}
|
|
309
403
|
|
|
310
|
-
// Call
|
|
311
404
|
const result = await modal({ component: MyModal });
|
|
312
405
|
```
|
|
313
406
|
|
|
314
407
|
```tsx
|
|
315
|
-
// WRONG
|
|
408
|
+
// WRONG — div 로 모달을 직접 만든 경우
|
|
316
409
|
function MyModal({ close }: { close: () => void }) {
|
|
317
410
|
return (
|
|
318
411
|
<div className="p-6 bg-white rounded-lg">
|
|
@@ -323,6 +416,13 @@ function MyModal({ close }: { close: () => void }) {
|
|
|
323
416
|
}
|
|
324
417
|
```
|
|
325
418
|
|
|
419
|
+
### Self-check (모달 작성 후 출력 직전 검증)
|
|
420
|
+
|
|
421
|
+
- [ ] props 타입이 `ModalPropsType<T>` 를 확장하고 있는가?
|
|
422
|
+
- [ ] `<ModalTemplate>` 에 `className={className}` 와 `layout={layout}` 을 그대로 넘겼는가?
|
|
423
|
+
- [ ] `<ModalTemplate>` 외부에 별도 wrapper `<div>` 를 두지 않았는가?
|
|
424
|
+
- [ ] 자체 wrap 클래스를 추가했다면 `cn(className, '...')` 으로 시스템 className 을 보존했는가?
|
|
425
|
+
|
|
326
426
|
## Toast Usage Rules
|
|
327
427
|
|
|
328
428
|
```tsx
|
|
@@ -985,11 +985,19 @@ WHEN TO USE:
|
|
|
985
985
|
|
|
986
986
|
PREFERRED API: use modal() / useModal() imperative API rather than mounting <ModalTemplate> directly. modal() handles stacking, focus return, ESC, and background scroll automatically.
|
|
987
987
|
|
|
988
|
+
SIZING (must read — most common AI mistake):
|
|
989
|
+
• The modal has TWO layers:
|
|
990
|
+
1. wrap = full-screen `position: fixed; inset: 0` flex container → controlled by `className`
|
|
991
|
+
2. panel = the actual dialog box rendered at the center → controlled by `innerClassName`
|
|
992
|
+
• Sizing utilities (`w-*`, `max-w-*`, `min-w-*`, `h-*`, `max-h-*`) MUST go on `innerClassName`.
|
|
993
|
+
• Putting `w-[400px] max-w-[92vw]` on `className` shrinks the wrap (the centering layer), NOT the panel — the panel keeps its default width and the modal looks broken.
|
|
994
|
+
|
|
988
995
|
ANTI-PATTERNS:
|
|
989
996
|
✗ Custom <div className="fixed inset-0"> → modal() (loses focus trap, a11y)
|
|
990
997
|
✗ Direct <ModalTemplate> mount in render tree → wrap with modal()
|
|
991
998
|
✗ Modal with no title for screen readers → always pass title prop
|
|
992
999
|
✗ Multiple modals stacking confusingly → use isAlone:true to close prior modals
|
|
1000
|
+
✗ <ModalTemplate className="w-[400px] max-w-[92vw]"> → put sizing on `innerClassName` instead
|
|
993
1001
|
|
|
994
1002
|
| Prop | Type | Default | Description |
|
|
995
1003
|
|---|---|---|---|
|
|
@@ -1000,15 +1008,16 @@ ANTI-PATTERNS:
|
|
|
1000
1008
|
| `dimClose` | `boolean` | `true` | Close on dim click |
|
|
1001
1009
|
| `hideHeader` | `boolean` | `false` | Hide header |
|
|
1002
1010
|
| `hideFooter` | `boolean` | `true` | Hide footer |
|
|
1011
|
+
| `hideCloseBtn` | `boolean` | `false` | Hide the X close button in the header |
|
|
1003
1012
|
| `footer` | `ReactNode` | - | Custom footer (ReactElement) |
|
|
1004
1013
|
| `animation` | `object` | - | Modal animation |
|
|
1005
1014
|
| `enableDrag` | `boolean` | `true` | Enable drag (bottom-sheet/draggable layouts) |
|
|
1006
1015
|
| `dragPersistKey` | `string` | - | Drag position persistence key |
|
|
1007
1016
|
| `close` | `ReactNode` | - | Modal close function (isAnimation?: boolean) => void (auto-injected) |
|
|
1008
1017
|
| `children` | `ReactNode` | - | Modal body (ReactNode, required) |
|
|
1009
|
-
| `className` | `string` | - | Root wrapper style |
|
|
1010
|
-
| `innerClassName` | `string` | - | Modal
|
|
1011
|
-
| `bodyClassName` | `string` | - | Body area style |
|
|
1018
|
+
| `className` | `string` | - | Root wrapper style — applied to the full-screen `position: fixed; inset: 0` flex container that centers the modal. NOTE: width / max-width / height utilities (`w-[400px]`, `max-w-[92vw]`, `h-*`) MUST NOT go here — they will resize the wrap layer, not the panel. Put sizing on `innerClassName` instead. |
|
|
1019
|
+
| `innerClassName` | `string` | - | Modal panel (the actual dialog box) style — apply ALL sizing utilities here: `w-[400px]`, `max-w-[92vw]`, `min-h-*`, `max-h-[80vh]`, `rounded-*`, panel background, etc. This is the most common override point. |
|
|
1020
|
+
| `bodyClassName` | `string` | - | Body area style (inside the panel, between header and footer) |
|
|
1012
1021
|
| `footerClassName` | `string` | - | Footer area style |
|
|
1013
1022
|
| `dimClassName` | `string` | - | Dim overlay style |
|
|
1014
1023
|
| `headerClassName` | `string` | - | Header area style |
|
|
@@ -1056,6 +1065,53 @@ function MyModal({ close, resolve }: { close: () => void; resolve: (value: any)
|
|
|
1056
1065
|
}
|
|
1057
1066
|
```
|
|
1058
1067
|
|
|
1068
|
+
### Sizing the modal (`className` vs `innerClassName`)
|
|
1069
|
+
|
|
1070
|
+
ModalTemplate has **two layers**:
|
|
1071
|
+
|
|
1072
|
+
| Prop | DOM target | Use for |
|
|
1073
|
+
|---|---|---|
|
|
1074
|
+
| `className` | full-screen wrap (`position: fixed; inset: 0` flex centering layer) | dim/centering overrides only |
|
|
1075
|
+
| `innerClassName` | the actual modal panel (the visible dialog box) | **all sizing**: `w-*`, `max-w-*`, `min-w-*`, `h-*`, `max-h-*`, panel `rounded-*`, panel padding |
|
|
1076
|
+
|
|
1077
|
+
Putting `w-[400px] max-w-[92vw]` on `className` shrinks the centering wrap, **not** the panel — the panel keeps its default width and the modal renders incorrectly. This is the single most common Modal mistake AI assistants make. **Always put sizing utilities on `innerClassName`.**
|
|
1078
|
+
|
|
1079
|
+
```tsx
|
|
1080
|
+
// ❌ WRONG — sizing on the wrap layer (panel size unchanged, layout breaks)
|
|
1081
|
+
<ModalTemplate
|
|
1082
|
+
title="Title"
|
|
1083
|
+
close={close}
|
|
1084
|
+
className="w-[400px] max-w-[92vw]"
|
|
1085
|
+
>
|
|
1086
|
+
...
|
|
1087
|
+
</ModalTemplate>
|
|
1088
|
+
|
|
1089
|
+
// ✅ CORRECT — sizing on the panel
|
|
1090
|
+
<ModalTemplate
|
|
1091
|
+
title="Title"
|
|
1092
|
+
close={close}
|
|
1093
|
+
innerClassName="w-[400px] max-w-[92vw]"
|
|
1094
|
+
>
|
|
1095
|
+
...
|
|
1096
|
+
</ModalTemplate>
|
|
1097
|
+
|
|
1098
|
+
// ✅ Larger modal with constrained height + scrollable body
|
|
1099
|
+
<ModalTemplate
|
|
1100
|
+
title="Long form"
|
|
1101
|
+
close={close}
|
|
1102
|
+
innerClassName="w-[640px] max-w-[92vw] max-h-[80vh]"
|
|
1103
|
+
bodyClassName="overflow-y-auto"
|
|
1104
|
+
>
|
|
1105
|
+
...
|
|
1106
|
+
</ModalTemplate>
|
|
1107
|
+
|
|
1108
|
+
// ✅ Same applies when calling via modal() — pass through props
|
|
1109
|
+
modal({
|
|
1110
|
+
component: MyModal,
|
|
1111
|
+
props: { innerClassName: 'w-[480px] max-w-[92vw]' },
|
|
1112
|
+
});
|
|
1113
|
+
```
|
|
1114
|
+
|
|
1059
1115
|
### Calling a Modal
|
|
1060
1116
|
|
|
1061
1117
|
```tsx
|
|
@@ -1086,6 +1142,7 @@ openModal({
|
|
|
1086
1142
|
- Do NOT create modal components without ModalTemplate (using plain `<div>`)
|
|
1087
1143
|
- Do NOT define the `close` prop manually (the system injects it automatically)
|
|
1088
1144
|
- Do NOT implement a separate dim/overlay inside the modal
|
|
1145
|
+
- Do NOT put sizing utilities (`w-*`, `max-w-*`, `min-w-*`, `h-*`, `max-h-*`) on `className` — they belong on `innerClassName` (the panel). `className` only styles the full-screen wrap.
|
|
1089
1146
|
|
|
1090
1147
|
---
|
|
1091
1148
|
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { useDraggableBottomSheet } from './chunk-
|
|
1
|
+
import { useDraggableBottomSheet } from './chunk-WBCXHGRL.mjs';
|
|
2
2
|
import { useDraggableWindow } from './chunk-4J3GCZ7W.mjs';
|
|
3
3
|
import { scrollRelease, scrollFreeze } from './chunk-54IA2P2Z.mjs';
|
|
4
|
-
import { useCheckDevice_default } from './chunk-
|
|
4
|
+
import { useCheckDevice_default } from './chunk-U6OEUBIF.mjs';
|
|
5
5
|
import { useClickOutside_default } from './chunk-OTGS6BDQ.mjs';
|
|
6
|
-
import { Modal_default } from './chunk-
|
|
6
|
+
import { Modal_default, defaultBreakPoints } from './chunk-FY2N42XN.mjs';
|
|
7
7
|
import { cn } from './chunk-MCKOWMLS.mjs';
|
|
8
8
|
import React, { forwardRef, useState, useMemo, useCallback, useEffect, useImperativeHandle, useRef, Suspense } from 'react';
|
|
9
9
|
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
@@ -184,7 +184,11 @@ var ModalPortal = ({ children, selector }) => {
|
|
|
184
184
|
return mounted && element && children ? createPortal(children, element) : null;
|
|
185
185
|
};
|
|
186
186
|
var ModalPortal_default = ModalPortal;
|
|
187
|
-
var ModalContainer = ({
|
|
187
|
+
var ModalContainer = ({
|
|
188
|
+
className = "",
|
|
189
|
+
breakPoints = defaultBreakPoints,
|
|
190
|
+
...defaultOption
|
|
191
|
+
}) => {
|
|
188
192
|
const [modals, setModals] = useState(Modal_default.modalList);
|
|
189
193
|
const setModalClose = useCallback((params) => {
|
|
190
194
|
if (!params) return;
|
|
@@ -408,6 +412,7 @@ var ModalTemplate = forwardRef(
|
|
|
408
412
|
desc,
|
|
409
413
|
hideHeader = false,
|
|
410
414
|
hideFooter = true,
|
|
415
|
+
hideCloseBtn = false,
|
|
411
416
|
dimClose,
|
|
412
417
|
footer,
|
|
413
418
|
close,
|
|
@@ -562,7 +567,7 @@ var ModalTemplate = forwardRef(
|
|
|
562
567
|
ref: modalHeaderRef,
|
|
563
568
|
onMouseDown: isDraggable && enableDrag ? windowDrag.handleMouseDown : void 0,
|
|
564
569
|
children: [
|
|
565
|
-
/* @__PURE__ */ jsx(
|
|
570
|
+
!hideCloseBtn && /* @__PURE__ */ jsx(
|
|
566
571
|
"button",
|
|
567
572
|
{
|
|
568
573
|
type: "button",
|
|
@@ -25,9 +25,9 @@ function useDraggableBottomSheet({
|
|
|
25
25
|
if (!enabled) return;
|
|
26
26
|
const touch = e.touches[0];
|
|
27
27
|
const target = e.currentTarget;
|
|
28
|
-
const modalInner = target.querySelector(".modal-inner");
|
|
28
|
+
const modalInner = target.querySelector(".nexus-modal-inner");
|
|
29
29
|
if (!modalInner) return;
|
|
30
|
-
const scrollableElement = e.target.closest(".modal-body");
|
|
30
|
+
const scrollableElement = e.target.closest(".nexus-modal-body");
|
|
31
31
|
if (scrollableElement && scrollableElement.scrollTop > 0) return;
|
|
32
32
|
dragStateRef.current = {
|
|
33
33
|
isDragging: true,
|
|
@@ -8,6 +8,11 @@ var defaultModalOption = {
|
|
|
8
8
|
animation: { name: "pop-fade", duration: 300 },
|
|
9
9
|
dimCloseEnable: true
|
|
10
10
|
};
|
|
11
|
+
var defaultBreakPoints = {
|
|
12
|
+
isDesktop: "(min-width: 1280px)",
|
|
13
|
+
isTablet: "screen and (min-width: 768px) and (max-width: 1279px)",
|
|
14
|
+
isMobile: "(max-width: 767px)"
|
|
15
|
+
};
|
|
11
16
|
|
|
12
17
|
// src/modal/Modal.ts
|
|
13
18
|
var Modal = class {
|
|
@@ -267,4 +272,4 @@ function useModal() {
|
|
|
267
272
|
};
|
|
268
273
|
}
|
|
269
274
|
|
|
270
|
-
export { Modal_default, useModal };
|
|
275
|
+
export { Modal_default, defaultBreakPoints, useModal };
|
|
@@ -3,10 +3,6 @@
|
|
|
3
3
|
var react = require('react');
|
|
4
4
|
|
|
5
5
|
// src/hooks/useCheckDevice.ts
|
|
6
|
-
function getMatchMedia(query) {
|
|
7
|
-
if (typeof window === "undefined") return false;
|
|
8
|
-
return window.matchMedia(query).matches;
|
|
9
|
-
}
|
|
10
6
|
var DEFAULT_BREAKPOINTS = {
|
|
11
7
|
mobile: 0,
|
|
12
8
|
tablet: 768,
|
|
@@ -47,8 +43,8 @@ function useCheckDevice(breakPoints) {
|
|
|
47
43
|
}, [serialized]);
|
|
48
44
|
const [result, setResult] = react.useState(() => {
|
|
49
45
|
const init = {};
|
|
50
|
-
for (const
|
|
51
|
-
init[key] =
|
|
46
|
+
for (const key of Object.keys(queries)) {
|
|
47
|
+
init[key] = false;
|
|
52
48
|
}
|
|
53
49
|
return init;
|
|
54
50
|
});
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var
|
|
3
|
+
var chunkF24AY3HI_js = require('./chunk-F24AY3HI.js');
|
|
4
4
|
var chunkNHDGKOAM_js = require('./chunk-NHDGKOAM.js');
|
|
5
5
|
var chunkT2IY2TSR_js = require('./chunk-T2IY2TSR.js');
|
|
6
|
-
var
|
|
6
|
+
var chunkNRO7I4EI_js = require('./chunk-NRO7I4EI.js');
|
|
7
7
|
var chunkINP2AH3B_js = require('./chunk-INP2AH3B.js');
|
|
8
|
-
var
|
|
8
|
+
var chunkTAHDSSA6_js = require('./chunk-TAHDSSA6.js');
|
|
9
9
|
var chunkCZC76ZD5_js = require('./chunk-CZC76ZD5.js');
|
|
10
10
|
var React = require('react');
|
|
11
11
|
var jsxRuntime = require('react/jsx-runtime');
|
|
@@ -51,7 +51,7 @@ var Component = ({
|
|
|
51
51
|
};
|
|
52
52
|
var ModalComponent = React.forwardRef(
|
|
53
53
|
({ className, modal, breakPoints, scrollFreeze: scrollFreeze2, scrollRelease: scrollRelease2, deleteModal, resolveModal }, ref) => {
|
|
54
|
-
const { isMobile } =
|
|
54
|
+
const { isMobile } = chunkNRO7I4EI_js.useCheckDevice_default(breakPoints);
|
|
55
55
|
const [status, setStatus] = React.useState("");
|
|
56
56
|
const clickOutSideTarget = React.useMemo(() => {
|
|
57
57
|
if (modal.props?.clickOutSideRef) {
|
|
@@ -190,21 +190,25 @@ var ModalPortal = ({ children, selector }) => {
|
|
|
190
190
|
return mounted && element && children ? reactDom.createPortal(children, element) : null;
|
|
191
191
|
};
|
|
192
192
|
var ModalPortal_default = ModalPortal;
|
|
193
|
-
var ModalContainer = ({
|
|
194
|
-
|
|
193
|
+
var ModalContainer = ({
|
|
194
|
+
className = "",
|
|
195
|
+
breakPoints = chunkTAHDSSA6_js.defaultBreakPoints,
|
|
196
|
+
...defaultOption
|
|
197
|
+
}) => {
|
|
198
|
+
const [modals, setModals] = React.useState(chunkTAHDSSA6_js.Modal_default.modalList);
|
|
195
199
|
const setModalClose = React.useCallback((params) => {
|
|
196
200
|
if (!params) return;
|
|
197
201
|
const { modalId, hasClose, close } = params;
|
|
198
202
|
if (!hasClose) {
|
|
199
|
-
|
|
203
|
+
chunkTAHDSSA6_js.Modal_default.modalList = chunkTAHDSSA6_js.Modal_default.modalList.map(
|
|
200
204
|
(m) => m.id === modalId ? { ...m, close } : m
|
|
201
205
|
);
|
|
202
206
|
}
|
|
203
207
|
}, []);
|
|
204
208
|
React.useEffect(() => {
|
|
205
|
-
|
|
209
|
+
chunkTAHDSSA6_js.Modal_default.observe({ options: defaultOption, observeFunction: setModals });
|
|
206
210
|
return () => {
|
|
207
|
-
|
|
211
|
+
chunkTAHDSSA6_js.Modal_default.unobserve();
|
|
208
212
|
};
|
|
209
213
|
}, [defaultOption]);
|
|
210
214
|
return /* @__PURE__ */ jsxRuntime.jsx("div", { id: "modal-wrapper", className, children: modals.map(
|
|
@@ -214,8 +218,8 @@ var ModalContainer = ({ className = "", breakPoints, ...defaultOption }) => {
|
|
|
214
218
|
ref: setModalClose,
|
|
215
219
|
modal: m,
|
|
216
220
|
breakPoints,
|
|
217
|
-
deleteModal: (id) =>
|
|
218
|
-
resolveModal: (params) =>
|
|
221
|
+
deleteModal: (id) => chunkTAHDSSA6_js.Modal_default.deleteModal(id),
|
|
222
|
+
resolveModal: (params) => chunkTAHDSSA6_js.Modal_default.resolveModal(params),
|
|
219
223
|
scrollFreeze: chunkT2IY2TSR_js.scrollFreeze,
|
|
220
224
|
scrollRelease: chunkT2IY2TSR_js.scrollRelease
|
|
221
225
|
},
|
|
@@ -226,8 +230,8 @@ var ModalContainer = ({ className = "", breakPoints, ...defaultOption }) => {
|
|
|
226
230
|
ref: setModalClose,
|
|
227
231
|
modal: m,
|
|
228
232
|
breakPoints,
|
|
229
|
-
deleteModal: (id) =>
|
|
230
|
-
resolveModal: (params) =>
|
|
233
|
+
deleteModal: (id) => chunkTAHDSSA6_js.Modal_default.deleteModal(id),
|
|
234
|
+
resolveModal: (params) => chunkTAHDSSA6_js.Modal_default.resolveModal(params),
|
|
231
235
|
scrollFreeze: chunkT2IY2TSR_js.scrollFreeze,
|
|
232
236
|
scrollRelease: chunkT2IY2TSR_js.scrollRelease
|
|
233
237
|
},
|
|
@@ -414,6 +418,7 @@ var ModalTemplate = React.forwardRef(
|
|
|
414
418
|
desc,
|
|
415
419
|
hideHeader = false,
|
|
416
420
|
hideFooter = true,
|
|
421
|
+
hideCloseBtn = false,
|
|
417
422
|
dimClose,
|
|
418
423
|
footer,
|
|
419
424
|
close,
|
|
@@ -427,7 +432,7 @@ var ModalTemplate = React.forwardRef(
|
|
|
427
432
|
const { dimCloseEnable } = getModalDefaultOption();
|
|
428
433
|
const isBottomSheet = layout === "bottom-sheet";
|
|
429
434
|
const isDraggable = layout === "draggable";
|
|
430
|
-
const dragHandlers =
|
|
435
|
+
const dragHandlers = chunkF24AY3HI_js.useDraggableBottomSheet({
|
|
431
436
|
enabled: isBottomSheet && enableDrag,
|
|
432
437
|
onClose: () => close(false),
|
|
433
438
|
threshold: 0.5,
|
|
@@ -568,7 +573,7 @@ var ModalTemplate = React.forwardRef(
|
|
|
568
573
|
ref: modalHeaderRef,
|
|
569
574
|
onMouseDown: isDraggable && enableDrag ? windowDrag.handleMouseDown : void 0,
|
|
570
575
|
children: [
|
|
571
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
576
|
+
!hideCloseBtn && /* @__PURE__ */ jsxRuntime.jsx(
|
|
572
577
|
"button",
|
|
573
578
|
{
|
|
574
579
|
type: "button",
|
|
@@ -619,28 +624,28 @@ var ModalTemplate_default = ModalTemplate;
|
|
|
619
624
|
var openModal = ((arg1, arg2 = {}) => {
|
|
620
625
|
if ("component" in arg1) {
|
|
621
626
|
const { component, props = {}, options = {} } = arg1;
|
|
622
|
-
return
|
|
627
|
+
return chunkTAHDSSA6_js.Modal_default.openModal({
|
|
623
628
|
component,
|
|
624
629
|
props: { ...props, ...options }
|
|
625
630
|
});
|
|
626
631
|
} else {
|
|
627
|
-
return
|
|
632
|
+
return chunkTAHDSSA6_js.Modal_default.openModal({
|
|
628
633
|
component: arg1,
|
|
629
634
|
props: arg2
|
|
630
635
|
});
|
|
631
636
|
}
|
|
632
637
|
});
|
|
633
638
|
var closeModal = (id, isAnimation) => {
|
|
634
|
-
return
|
|
639
|
+
return chunkTAHDSSA6_js.Modal_default.closeModal(id, isAnimation);
|
|
635
640
|
};
|
|
636
641
|
var checkModal = (params) => {
|
|
637
|
-
return
|
|
642
|
+
return chunkTAHDSSA6_js.Modal_default.checkModal(params);
|
|
638
643
|
};
|
|
639
644
|
var resetModal = () => {
|
|
640
|
-
return
|
|
645
|
+
return chunkTAHDSSA6_js.Modal_default.resetModal();
|
|
641
646
|
};
|
|
642
647
|
var getModalDefaultOption = () => {
|
|
643
|
-
return
|
|
648
|
+
return chunkTAHDSSA6_js.Modal_default.getDefaultOption();
|
|
644
649
|
};
|
|
645
650
|
|
|
646
651
|
exports.ModalContainer_default = ModalContainer_default;
|
|
@@ -10,6 +10,11 @@ var defaultModalOption = {
|
|
|
10
10
|
animation: { name: "pop-fade", duration: 300 },
|
|
11
11
|
dimCloseEnable: true
|
|
12
12
|
};
|
|
13
|
+
var defaultBreakPoints = {
|
|
14
|
+
isDesktop: "(min-width: 1280px)",
|
|
15
|
+
isTablet: "screen and (min-width: 768px) and (max-width: 1279px)",
|
|
16
|
+
isMobile: "(max-width: 767px)"
|
|
17
|
+
};
|
|
13
18
|
|
|
14
19
|
// src/modal/Modal.ts
|
|
15
20
|
var Modal = class {
|
|
@@ -270,4 +275,5 @@ function useModal() {
|
|
|
270
275
|
}
|
|
271
276
|
|
|
272
277
|
exports.Modal_default = Modal_default;
|
|
278
|
+
exports.defaultBreakPoints = defaultBreakPoints;
|
|
273
279
|
exports.useModal = useModal;
|
|
@@ -1,10 +1,6 @@
|
|
|
1
1
|
import { useMemo, useState, useEffect } from 'react';
|
|
2
2
|
|
|
3
3
|
// src/hooks/useCheckDevice.ts
|
|
4
|
-
function getMatchMedia(query) {
|
|
5
|
-
if (typeof window === "undefined") return false;
|
|
6
|
-
return window.matchMedia(query).matches;
|
|
7
|
-
}
|
|
8
4
|
var DEFAULT_BREAKPOINTS = {
|
|
9
5
|
mobile: 0,
|
|
10
6
|
tablet: 768,
|
|
@@ -45,8 +41,8 @@ function useCheckDevice(breakPoints) {
|
|
|
45
41
|
}, [serialized]);
|
|
46
42
|
const [result, setResult] = useState(() => {
|
|
47
43
|
const init = {};
|
|
48
|
-
for (const
|
|
49
|
-
init[key] =
|
|
44
|
+
for (const key of Object.keys(queries)) {
|
|
45
|
+
init[key] = false;
|
|
50
46
|
}
|
|
51
47
|
return init;
|
|
52
48
|
});
|
|
@@ -23,9 +23,9 @@ function useDraggableBottomSheet({
|
|
|
23
23
|
if (!enabled) return;
|
|
24
24
|
const touch = e.touches[0];
|
|
25
25
|
const target = e.currentTarget;
|
|
26
|
-
const modalInner = target.querySelector(".modal-inner");
|
|
26
|
+
const modalInner = target.querySelector(".nexus-modal-inner");
|
|
27
27
|
if (!modalInner) return;
|
|
28
|
-
const scrollableElement = e.target.closest(".modal-body");
|
|
28
|
+
const scrollableElement = e.target.closest(".nexus-modal-body");
|
|
29
29
|
if (scrollableElement && scrollableElement.scrollTop > 0) return;
|
|
30
30
|
dragStateRef.current = {
|
|
31
31
|
isDragging: true,
|