@jogak/ui 0.1.0-alpha.7.1 → 0.1.0-alpha.8
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/CHANGELOG.md +23 -0
- package/README.md +41 -23
- package/dist/app/App.d.ts +10 -5
- package/dist/components/Preview/IframeMount.d.ts +17 -24
- package/dist/components/Preview/index.d.ts +10 -5
- package/dist/host/index.d.ts +12 -4
- package/dist/host/index.js +1 -1
- package/dist/host/index.mjs +30 -30
- package/dist/index.js +1 -1
- package/dist/index.mjs +306 -287
- package/package.json +3 -3
- package/src/app/App.tsx +12 -5
- package/src/app/main.tsx +11 -7
- package/src/components/Preview/IframeMount.tsx +54 -43
- package/src/components/Preview/index.tsx +26 -12
- package/src/vite-env.d.ts +7 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jogak/ui",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.8",
|
|
4
4
|
"description": "Showcase viewer UI for Jogak — Sidebar / Preview / Controls / Actions and the JogakApp shell.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"jogak",
|
|
@@ -65,8 +65,8 @@
|
|
|
65
65
|
"prism-react-renderer": "^2.4.1",
|
|
66
66
|
"tailwindcss": "^4.0.0",
|
|
67
67
|
"@tailwindcss/vite": "^4.0.0",
|
|
68
|
-
"@jogak/core": "0.1.0-alpha.
|
|
69
|
-
"@jogak/react": "0.1.0-alpha.
|
|
68
|
+
"@jogak/core": "0.1.0-alpha.8",
|
|
69
|
+
"@jogak/react": "0.1.0-alpha.8"
|
|
70
70
|
},
|
|
71
71
|
"devDependencies": {
|
|
72
72
|
"@types/node": "^20.14.0",
|
package/src/app/App.tsx
CHANGED
|
@@ -21,15 +21,20 @@ export interface JogakAppProps {
|
|
|
21
21
|
readonly metas?: readonly RegistryEntryMeta[]
|
|
22
22
|
readonly codeTheme?: string
|
|
23
23
|
/**
|
|
24
|
-
* 알파.
|
|
24
|
+
* 알파.8: Preview 영역 격리 모드. default `'iframe'`.
|
|
25
25
|
*
|
|
26
|
-
* - `'
|
|
27
|
-
* - `'shadow'` — ShadowRoot 안에 마운트. 사용자
|
|
28
|
-
* - `'
|
|
26
|
+
* - `'iframe'` (default) — 사용자 vite 정상 client(iframe)에 마운트. 사용자 utility 정상 컴파일.
|
|
27
|
+
* - `'shadow'` (deprecated) — ShadowRoot 안에 마운트. 사용자 utility 미적용.
|
|
28
|
+
* - `'none'` (deprecated) — chrome 같은 document에 렌더. 알파.6까지의 동작.
|
|
29
29
|
*
|
|
30
30
|
* 자세한 트레이드오프는 `@jogak/ui` README의 "previewIsolation 사용 가이드" 참조.
|
|
31
31
|
*/
|
|
32
32
|
readonly previewIsolation?: 'none' | 'shadow' | 'iframe'
|
|
33
|
+
/**
|
|
34
|
+
* 알파.8: 사용자 vite spawn URL. iframe `src` base로 사용.
|
|
35
|
+
* 빈 문자열 시 fallback (jogak SPA Vite scope의 preview-frame.tsx).
|
|
36
|
+
*/
|
|
37
|
+
readonly userViteUrl?: string
|
|
33
38
|
}
|
|
34
39
|
|
|
35
40
|
function readUrlParams(): { entryId: string; jogakName: string | null } | null {
|
|
@@ -52,7 +57,8 @@ export function JogakApp({
|
|
|
52
57
|
entries,
|
|
53
58
|
metas,
|
|
54
59
|
codeTheme = 'vsDark',
|
|
55
|
-
previewIsolation = '
|
|
60
|
+
previewIsolation = 'iframe',
|
|
61
|
+
userViteUrl = '',
|
|
56
62
|
}: JogakAppProps = {}): ReactElement {
|
|
57
63
|
// ── 4가지 모드 결정 (계약 §5.2) ─────────────────────────────────────
|
|
58
64
|
// 1) entries가 주어지면: 새 ComponentRegistry에 register (eager, 기존 동작)
|
|
@@ -156,6 +162,7 @@ export function JogakApp({
|
|
|
156
162
|
codeTheme={codeTheme}
|
|
157
163
|
onResolveJogak={handleResolveJogak}
|
|
158
164
|
previewIsolation={previewIsolation}
|
|
165
|
+
userViteUrl={userViteUrl}
|
|
159
166
|
/>
|
|
160
167
|
) : (
|
|
161
168
|
<div className="jogak:flex jogak:items-center jogak:justify-center jogak:h-full jogak:text-[var(--jogak-color-fg-subtle)]">
|
package/src/app/main.tsx
CHANGED
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
import { StrictMode } from 'react'
|
|
2
2
|
import { createRoot } from 'react-dom/client'
|
|
3
3
|
import 'virtual:jogak'
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
_jogakCodeTheme,
|
|
6
|
+
_jogakPreviewIsolation,
|
|
7
|
+
_jogakUserViteUrl,
|
|
8
|
+
} from 'virtual:jogak'
|
|
5
9
|
import '../styles/jogak.css'
|
|
6
10
|
import { JogakApp } from './App.js'
|
|
7
11
|
|
|
8
|
-
// 알파.
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
// 정적 분석하여 별도 chunk + css HMR 표준 경로로 처리한다.
|
|
12
|
+
// 알파.8: 사용자 globalCss는 사용자 vite scope(iframe entry)에서 처리되므로
|
|
13
|
+
// jogak SPA outer document에는 import하지 않는다 — chrome 격리 보존.
|
|
14
|
+
//
|
|
15
|
+
// 'none' 모드(deprecated): 알파.7.1 동작 유지가 필요한 사용자만 명시 사용.
|
|
16
|
+
// 이 경우만 outer document에 사용자 globalCss inject.
|
|
14
17
|
if (_jogakPreviewIsolation === 'none') {
|
|
15
18
|
await import('virtual:jogak/global-css')
|
|
16
19
|
}
|
|
@@ -23,6 +26,7 @@ createRoot(rootEl).render(
|
|
|
23
26
|
<JogakApp
|
|
24
27
|
codeTheme={_jogakCodeTheme}
|
|
25
28
|
previewIsolation={_jogakPreviewIsolation}
|
|
29
|
+
userViteUrl={_jogakUserViteUrl}
|
|
26
30
|
/>
|
|
27
31
|
</StrictMode>,
|
|
28
32
|
)
|
|
@@ -1,81 +1,92 @@
|
|
|
1
|
-
import { useEffect, useRef } from 'react'
|
|
1
|
+
import { useEffect, useRef, useState } from 'react'
|
|
2
2
|
import type { ReactElement } from 'react'
|
|
3
3
|
import type { RegistryEntry } from '@jogak/core'
|
|
4
4
|
|
|
5
5
|
export interface IframeMountProps {
|
|
6
6
|
readonly entry: RegistryEntry
|
|
7
7
|
readonly args: Readonly<Record<string, unknown>>
|
|
8
|
+
/**
|
|
9
|
+
* 알파.8: 사용자 vite spawn URL (예: `http://localhost:5174`).
|
|
10
|
+
* 빈 문자열 시 fallback (jogak SPA Vite scope의 `/preview-frame.html`).
|
|
11
|
+
*/
|
|
12
|
+
readonly userViteUrl: string
|
|
8
13
|
readonly className?: string
|
|
9
14
|
readonly 'data-testid'?: string
|
|
10
15
|
}
|
|
11
16
|
|
|
12
|
-
interface SetPropsArgs {
|
|
13
|
-
readonly entry: RegistryEntry
|
|
14
|
-
readonly args: Readonly<Record<string, unknown>>
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
declare global {
|
|
18
|
-
interface Window {
|
|
19
|
-
__jogak_setProps__?: (args: SetPropsArgs) => void
|
|
20
|
-
__jogak_unmount__?: () => void
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
|
|
24
17
|
/**
|
|
25
|
-
* 알파.
|
|
18
|
+
* 알파.8: previewIsolation='iframe' 모드의 mount 컴포넌트.
|
|
26
19
|
*
|
|
27
|
-
*
|
|
28
|
-
* -
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
* - entry/args 변경 시 setProps 재호출 (load 완료 이후).
|
|
20
|
+
* 통신:
|
|
21
|
+
* - 사용자 vite spawn URL이 주어지면(`userViteUrl !== ''`) iframe src를
|
|
22
|
+
* `${userViteUrl}/__jogak_preview__/index.html` (cross-origin)로 설정.
|
|
23
|
+
* - 동일 origin fallback 시 `/preview-frame.html` (jogak SPA Vite scope).
|
|
32
24
|
*
|
|
33
|
-
*
|
|
34
|
-
* - iframe
|
|
35
|
-
*
|
|
36
|
-
* - previewIsolation 모드 자체 변경은 가상 모듈 invalidate → full reload.
|
|
25
|
+
* 양쪽 모두 postMessage로 통신:
|
|
26
|
+
* - 부모 → iframe: `{ type: 'jogak:setProps', entryId, args }` | `{ type: 'jogak:unmount' }`
|
|
27
|
+
* - iframe → 부모: `{ type: 'jogak:ready' }` | `{ type: 'jogak:rendered', entryId }`
|
|
37
28
|
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
29
|
+
* `entry`는 객체가 아닌 **id만 전달** — iframe 안에서 `defaultRegistry.requestEntry(id)`로
|
|
30
|
+
* dynamic import. 사용자 vite scope의 entry 가상 모듈이 사용자 컴포넌트를 fetch하므로
|
|
31
|
+
* 사용자 plugins(@tailwindcss/vite, custom alias 등)이 정상 작동.
|
|
40
32
|
*/
|
|
41
33
|
export function IframeMount({
|
|
42
34
|
entry,
|
|
43
35
|
args,
|
|
36
|
+
userViteUrl,
|
|
44
37
|
className,
|
|
45
38
|
'data-testid': dataTestId,
|
|
46
39
|
}: IframeMountProps): ReactElement {
|
|
47
40
|
const iframeRef = useRef<HTMLIFrameElement | null>(null)
|
|
48
|
-
const
|
|
41
|
+
const [ready, setReady] = useState(false)
|
|
42
|
+
|
|
43
|
+
const src =
|
|
44
|
+
userViteUrl !== ''
|
|
45
|
+
? `${userViteUrl}/__jogak_preview__/index.html`
|
|
46
|
+
: '/preview-frame.html'
|
|
49
47
|
|
|
50
|
-
// iframe
|
|
48
|
+
// postMessage 리스너 — iframe contentWindow 일치성 검증 후 처리.
|
|
51
49
|
useEffect(() => {
|
|
50
|
+
const handler = (event: MessageEvent): void => {
|
|
51
|
+
const iframe = iframeRef.current
|
|
52
|
+
if (iframe === null) return
|
|
53
|
+
if (event.source !== iframe.contentWindow) return
|
|
54
|
+
const data = event.data
|
|
55
|
+
if (data == null || typeof data !== 'object') return
|
|
56
|
+
if (data.type === 'jogak:ready') setReady(true)
|
|
57
|
+
}
|
|
58
|
+
window.addEventListener('message', handler)
|
|
59
|
+
return () => {
|
|
60
|
+
window.removeEventListener('message', handler)
|
|
61
|
+
}
|
|
62
|
+
}, [])
|
|
63
|
+
|
|
64
|
+
// iframe ready 또는 entry/args 변경 시 setProps.
|
|
65
|
+
useEffect(() => {
|
|
66
|
+
if (!ready) return
|
|
52
67
|
const iframe = iframeRef.current
|
|
53
68
|
if (iframe === null) return
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
69
|
+
iframe.contentWindow?.postMessage(
|
|
70
|
+
{ type: 'jogak:setProps', entryId: entry.id, args },
|
|
71
|
+
'*',
|
|
72
|
+
)
|
|
73
|
+
}, [ready, entry, args])
|
|
74
|
+
|
|
75
|
+
// unmount 시 unmount 메시지 (race 회피 microtask defer).
|
|
76
|
+
useEffect(() => {
|
|
77
|
+
const iframe = iframeRef.current
|
|
59
78
|
return () => {
|
|
60
|
-
iframe
|
|
61
|
-
// 알파.7.1: unmount race 회피 — iframe contentWindow 정리도 microtask defer.
|
|
79
|
+
if (iframe === null) return
|
|
62
80
|
queueMicrotask(() => {
|
|
63
|
-
iframe.contentWindow?.
|
|
81
|
+
iframe.contentWindow?.postMessage({ type: 'jogak:unmount' }, '*')
|
|
64
82
|
})
|
|
65
83
|
}
|
|
66
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
67
84
|
}, [])
|
|
68
85
|
|
|
69
|
-
// entry/args 변경 시 setProps 재호출 (load 후에만)
|
|
70
|
-
useEffect(() => {
|
|
71
|
-
if (!readyRef.current) return
|
|
72
|
-
iframeRef.current?.contentWindow?.__jogak_setProps__?.({ entry, args })
|
|
73
|
-
}, [entry, args])
|
|
74
|
-
|
|
75
86
|
return (
|
|
76
87
|
<iframe
|
|
77
88
|
ref={iframeRef}
|
|
78
|
-
src=
|
|
89
|
+
src={src}
|
|
79
90
|
title="Preview"
|
|
80
91
|
className={className}
|
|
81
92
|
data-testid={dataTestId}
|
|
@@ -24,13 +24,18 @@ export interface PreviewProps {
|
|
|
24
24
|
*/
|
|
25
25
|
readonly onResolveJogak?: (entryId: string, jogakName: string) => void
|
|
26
26
|
/**
|
|
27
|
-
* 알파.
|
|
27
|
+
* 알파.8: Preview 영역 격리 모드. default `'iframe'`.
|
|
28
28
|
*
|
|
29
|
-
* - `'
|
|
30
|
-
* - `'shadow'` — ShadowRoot에 마운트. 사용자
|
|
31
|
-
* - `'
|
|
29
|
+
* - `'iframe'` (default) — 사용자 vite scope에 마운트. 사용자 utility 정상 컴파일.
|
|
30
|
+
* - `'shadow'` (deprecated) — ShadowRoot에 마운트. 사용자 utility 미적용.
|
|
31
|
+
* - `'none'` (deprecated) — chrome과 같은 document에 렌더.
|
|
32
32
|
*/
|
|
33
33
|
readonly previewIsolation?: 'none' | 'shadow' | 'iframe'
|
|
34
|
+
/**
|
|
35
|
+
* 알파.8: 사용자 vite spawn URL. iframe `src` base.
|
|
36
|
+
* 빈 문자열 시 fallback (jogak SPA Vite scope의 `/preview-frame.html`).
|
|
37
|
+
*/
|
|
38
|
+
readonly userViteUrl?: string
|
|
34
39
|
}
|
|
35
40
|
|
|
36
41
|
type ViewportKey = 'mobile' | 'tablet' | 'desktop'
|
|
@@ -120,7 +125,8 @@ export function Preview({
|
|
|
120
125
|
onReset,
|
|
121
126
|
codeTheme,
|
|
122
127
|
onResolveJogak,
|
|
123
|
-
previewIsolation = '
|
|
128
|
+
previewIsolation = 'iframe',
|
|
129
|
+
userViteUrl = '',
|
|
124
130
|
}: PreviewProps): ReactElement {
|
|
125
131
|
const state = useEntry(entryId)
|
|
126
132
|
const [viewport, setViewport] = useState<ViewportKey>('desktop')
|
|
@@ -187,6 +193,7 @@ export function Preview({
|
|
|
187
193
|
onBottomTabChange={setBottomTab}
|
|
188
194
|
prismTheme={prismTheme}
|
|
189
195
|
previewIsolation={previewIsolation}
|
|
196
|
+
userViteUrl={userViteUrl}
|
|
190
197
|
/>
|
|
191
198
|
)
|
|
192
199
|
}
|
|
@@ -275,6 +282,7 @@ interface ReadyFrameProps {
|
|
|
275
282
|
readonly onBottomTabChange: (tab: 'controls' | 'actions') => void
|
|
276
283
|
readonly prismTheme: PrismTheme
|
|
277
284
|
readonly previewIsolation: 'none' | 'shadow' | 'iframe'
|
|
285
|
+
readonly userViteUrl: string
|
|
278
286
|
}
|
|
279
287
|
|
|
280
288
|
function ReadyFrame({
|
|
@@ -292,6 +300,7 @@ function ReadyFrame({
|
|
|
292
300
|
onBottomTabChange,
|
|
293
301
|
prismTheme,
|
|
294
302
|
previewIsolation,
|
|
303
|
+
userViteUrl,
|
|
295
304
|
}: ReadyFrameProps): ReactElement {
|
|
296
305
|
// jogakName이 비어있으면 (deep link `?entry=...&jogak` 누락) 첫 jogak로 보정.
|
|
297
306
|
const resolvedJogakName = jogakName ?? entry.jogaks[0]?.name ?? null
|
|
@@ -364,6 +373,7 @@ function ReadyFrame({
|
|
|
364
373
|
source={entry.source}
|
|
365
374
|
theme={prismTheme}
|
|
366
375
|
previewIsolation={previewIsolation}
|
|
376
|
+
userViteUrl={userViteUrl}
|
|
367
377
|
/>
|
|
368
378
|
</div>
|
|
369
379
|
</div>
|
|
@@ -514,18 +524,19 @@ interface JogakRendererProps {
|
|
|
514
524
|
readonly source: string | undefined
|
|
515
525
|
readonly theme: PrismTheme
|
|
516
526
|
readonly previewIsolation: 'none' | 'shadow' | 'iframe'
|
|
527
|
+
readonly userViteUrl: string
|
|
517
528
|
}
|
|
518
529
|
|
|
519
530
|
/**
|
|
520
|
-
* 알파.
|
|
531
|
+
* 알파.8: previewIsolation 모드별로 사용자 콘텐츠 마운트 방식을 분기한다.
|
|
521
532
|
*
|
|
522
|
-
* - `'
|
|
523
|
-
* - `'shadow'` — `<ShadowMount>` 안에
|
|
524
|
-
* - `'
|
|
533
|
+
* - `'iframe'` (default) — 사용자 vite scope의 `<IframeMount>`로 별도 document.
|
|
534
|
+
* - `'shadow'` (deprecated) — `<ShadowMount>` 안에 마운트.
|
|
535
|
+
* - `'none'` (deprecated) — 같은 document에 직접 마운트.
|
|
525
536
|
*
|
|
526
537
|
* Show source 토글, 코드 패널 등 chrome 부분은 모드 무관하게 외부에 둔다.
|
|
527
538
|
*/
|
|
528
|
-
function JogakRenderer({ entry, args, source, theme, previewIsolation }: JogakRendererProps): ReactElement {
|
|
539
|
+
function JogakRenderer({ entry, args, source, theme, previewIsolation, userViteUrl }: JogakRendererProps): ReactElement {
|
|
529
540
|
const [showCode, setShowCode] = useState(false)
|
|
530
541
|
|
|
531
542
|
const previewBody = (
|
|
@@ -534,6 +545,7 @@ function JogakRenderer({ entry, args, source, theme, previewIsolation }: JogakRe
|
|
|
534
545
|
entry={entry}
|
|
535
546
|
args={args}
|
|
536
547
|
previewIsolation={previewIsolation}
|
|
548
|
+
userViteUrl={userViteUrl}
|
|
537
549
|
/>
|
|
538
550
|
<button
|
|
539
551
|
type="button"
|
|
@@ -575,13 +587,14 @@ interface PreviewMountProps {
|
|
|
575
587
|
readonly entry: RegistryEntry
|
|
576
588
|
readonly args: Readonly<Record<string, unknown>>
|
|
577
589
|
readonly previewIsolation: 'none' | 'shadow' | 'iframe'
|
|
590
|
+
readonly userViteUrl: string
|
|
578
591
|
}
|
|
579
592
|
|
|
580
593
|
const PREVIEW_HOST_CLASS =
|
|
581
594
|
'jogak:border jogak:border-dashed jogak:border-[var(--jogak-color-border)] ' +
|
|
582
595
|
'jogak:rounded-[var(--jogak-radius-xl)] jogak:p-4 jogak:pb-9'
|
|
583
596
|
|
|
584
|
-
function PreviewMount({ entry, args, previewIsolation }: PreviewMountProps): ReactElement {
|
|
597
|
+
function PreviewMount({ entry, args, previewIsolation, userViteUrl }: PreviewMountProps): ReactElement {
|
|
585
598
|
if (previewIsolation === 'shadow') {
|
|
586
599
|
return (
|
|
587
600
|
<ShadowMount
|
|
@@ -598,13 +611,14 @@ function PreviewMount({ entry, args, previewIsolation }: PreviewMountProps): Rea
|
|
|
598
611
|
<IframeMount
|
|
599
612
|
entry={entry}
|
|
600
613
|
args={args}
|
|
614
|
+
userViteUrl={userViteUrl}
|
|
601
615
|
data-testid="preview-content"
|
|
602
616
|
className={`${PREVIEW_HOST_CLASS} jogak:block jogak:w-full jogak:bg-transparent jogak:min-h-[256px]`}
|
|
603
617
|
/>
|
|
604
618
|
)
|
|
605
619
|
}
|
|
606
620
|
|
|
607
|
-
// 'none' —
|
|
621
|
+
// 'none' — deprecated 경로 (알파.7.1 동등 동작 보존, back-compat)
|
|
608
622
|
return <NoneAdapterContent entry={entry} args={args} />
|
|
609
623
|
}
|
|
610
624
|
|
package/src/vite-env.d.ts
CHANGED
|
@@ -4,10 +4,15 @@ declare module 'virtual:jogak' {
|
|
|
4
4
|
/** 플러그인 설정에서 지정한 prism-react-renderer 테마 이름 */
|
|
5
5
|
export const _jogakCodeTheme: string
|
|
6
6
|
/**
|
|
7
|
-
* 알파.
|
|
8
|
-
* `JogakPluginOptions.previewIsolation` (default '
|
|
7
|
+
* 알파.8: Preview 영역 격리 모드 ('none' | 'shadow' | 'iframe').
|
|
8
|
+
* `JogakPluginOptions.previewIsolation` (default 'iframe')의 literal emit.
|
|
9
9
|
*/
|
|
10
10
|
export const _jogakPreviewIsolation: 'none' | 'shadow' | 'iframe'
|
|
11
|
+
/**
|
|
12
|
+
* 알파.8: 사용자 vite spawn URL. iframe `src` base로 사용 (예: `http://localhost:5174`).
|
|
13
|
+
* 빈 문자열 시 fallback (jogak SPA Vite scope의 preview-frame.tsx).
|
|
14
|
+
*/
|
|
15
|
+
export const _jogakUserViteUrl: string
|
|
11
16
|
}
|
|
12
17
|
|
|
13
18
|
declare module 'virtual:jogak/global-css' {
|