@jogak/ui 0.1.0-alpha.4 → 0.1.0-alpha.5

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.
@@ -1,5 +1,6 @@
1
1
  import { useEffect, useRef, useState } from 'react'
2
2
  import type { ReactElement, CSSProperties } from 'react'
3
+ import clsx from 'clsx'
3
4
  import { Highlight, themes } from 'prism-react-renderer'
4
5
  import type { PrismTheme } from 'prism-react-renderer'
5
6
  import { reactAdapter, useEntry } from '@jogak/react'
@@ -25,6 +26,12 @@ export interface PreviewProps {
25
26
  type ViewportKey = 'mobile' | 'tablet' | 'desktop'
26
27
  type BgMode = 'white' | 'dark' | 'transparent'
27
28
 
29
+ /**
30
+ * dynamic style + CSS variable 주입을 위한 React `CSSProperties` 확장 타입
31
+ * (api-contracts 알파.5 PR 2 §6.1).
32
+ */
33
+ type CSSVarStyle = CSSProperties & Record<`--${string}`, string | number>
34
+
28
35
  const VIEWPORT_WIDTHS: Record<ViewportKey, number | 'none'> = {
29
36
  mobile: 375,
30
37
  tablet: 768,
@@ -37,22 +44,46 @@ const VIEWPORT_LABELS: Record<ViewportKey, string> = {
37
44
  desktop: 'Desktop',
38
45
  }
39
46
 
40
- const BG_STYLES: Record<BgMode, CSSProperties> = {
41
- white: { background: '#ffffff' },
42
- dark: { background: '#1f2937' },
47
+ /**
48
+ * bgMode별 캔버스 background 표현 — 4개 longhand CSS variable로 분해.
49
+ *
50
+ * v4 background shorthand arbitrary value(`bg-[...]`)는 ambiguous 하므로
51
+ * `bg-[image:...]`, `bg-[length:...]`, `bg-[position:...]` longhand hint를 사용해야 한다.
52
+ * 따라서 `BG_STYLES` (CSSProperties spread)를 폐기하고 mode별 변수 묶음만 정의한다
53
+ * (api-contracts 알파.5 PR 2 §3.2 결정 B).
54
+ */
55
+ const BG_VARS: Record<BgMode, CSSVarStyle> = {
56
+ white: {
57
+ '--jogak-canvas-bg': '#ffffff',
58
+ '--jogak-canvas-bg-image': 'none',
59
+ '--jogak-canvas-bg-size': 'auto',
60
+ '--jogak-canvas-bg-position': '0 0',
61
+ },
62
+ dark: {
63
+ '--jogak-canvas-bg': '#1f2937',
64
+ '--jogak-canvas-bg-image': 'none',
65
+ '--jogak-canvas-bg-size': 'auto',
66
+ '--jogak-canvas-bg-position': '0 0',
67
+ },
43
68
  transparent: {
44
- backgroundImage: [
45
- 'linear-gradient(45deg, #e2e8f0 25%, transparent 25%)',
46
- 'linear-gradient(-45deg, #e2e8f0 25%, transparent 25%)',
47
- 'linear-gradient(45deg, transparent 75%, #e2e8f0 75%)',
69
+ '--jogak-canvas-bg': '#ffffff',
70
+ '--jogak-canvas-bg-image':
71
+ 'linear-gradient(45deg, #e2e8f0 25%, transparent 25%), ' +
72
+ 'linear-gradient(-45deg, #e2e8f0 25%, transparent 25%), ' +
73
+ 'linear-gradient(45deg, transparent 75%, #e2e8f0 75%), ' +
48
74
  'linear-gradient(-45deg, transparent 75%, #e2e8f0 75%)',
49
- ].join(', '),
50
- backgroundSize: '16px 16px',
51
- backgroundPosition: '0 0, 0 8px, 8px -8px, -8px 0px',
52
- backgroundColor: '#ffffff',
75
+ '--jogak-canvas-bg-size': '16px 16px',
76
+ '--jogak-canvas-bg-position': '0 0, 0 8px, 8px -8px, -8px 0px',
53
77
  },
54
78
  }
55
79
 
80
+ /** 캔버스/미니버튼 공통 — 모드 무관. BG_VARS 가 변수 값을 mode별로 swap. */
81
+ const CANVAS_BG_CLASS =
82
+ 'jogak:bg-[var(--jogak-canvas-bg)] ' +
83
+ 'jogak:bg-[image:var(--jogak-canvas-bg-image)] ' +
84
+ 'jogak:bg-[length:var(--jogak-canvas-bg-size)] ' +
85
+ 'jogak:bg-[position:var(--jogak-canvas-bg-position)]'
86
+
56
87
  /** 캔버스 영역 minHeight — loading/ready 사이 layout shift 방지 (계약 §10). */
57
88
  const CANVAS_MIN_HEIGHT = 320
58
89
 
@@ -90,7 +121,10 @@ export function Preview({
90
121
  // ── unknown ───────────────────────────────────────────────
91
122
  if (state.status === 'unknown') {
92
123
  return (
93
- <div data-testid="preview-not-found" style={{ padding: 24, color: '#ef4444' }}>
124
+ <div
125
+ data-testid="preview-not-found"
126
+ className="jogak:p-6 jogak:text-[var(--jogak-color-error)]"
127
+ >
94
128
  Entry not found: {entryId}
95
129
  </div>
96
130
  )
@@ -101,30 +135,10 @@ export function Preview({
101
135
  return (
102
136
  <div
103
137
  data-testid="preview-error"
104
- style={{
105
- padding: 24,
106
- color: '#b91c1c',
107
- background: '#fef2f2',
108
- height: '100%',
109
- display: 'flex',
110
- flexDirection: 'column',
111
- gap: 12,
112
- alignItems: 'flex-start',
113
- }}
138
+ className="jogak:p-6 jogak:text-[var(--jogak-color-error-fg)] jogak:bg-[var(--jogak-color-bg-error)] jogak:h-full jogak:flex jogak:flex-col jogak:gap-3 jogak:items-start"
114
139
  >
115
- <div style={{ fontWeight: 600 }}>Failed to load entry: {entryId}</div>
116
- <pre
117
- style={{
118
- margin: 0,
119
- padding: 12,
120
- background: '#fff',
121
- border: '1px solid #fecaca',
122
- borderRadius: 6,
123
- fontSize: 12,
124
- whiteSpace: 'pre-wrap',
125
- maxWidth: '100%',
126
- }}
127
- >
140
+ <div className="jogak:font-semibold">Failed to load entry: {entryId}</div>
141
+ <pre className="jogak:m-0 jogak:p-3 jogak:bg-[var(--jogak-color-bg)] jogak:border jogak:border-[var(--jogak-color-error-border)] jogak:rounded-[var(--jogak-radius-lg)] jogak:text-[12px] jogak:whitespace-pre-wrap jogak:max-w-full">
128
142
  {state.error.message}
129
143
  </pre>
130
144
  </div>
@@ -190,7 +204,7 @@ function LoadingFrame({
190
204
  return (
191
205
  <div
192
206
  data-testid="preview-loading"
193
- style={{ display: 'flex', flexDirection: 'column', height: '100%' }}
207
+ className="jogak:flex jogak:flex-col jogak:h-full"
194
208
  >
195
209
  <Toolbar
196
210
  title={meta.title}
@@ -203,44 +217,31 @@ function LoadingFrame({
203
217
  onReset={() => {}}
204
218
  />
205
219
  <div
206
- style={{
207
- flex: 1,
208
- minHeight: CANVAS_MIN_HEIGHT,
209
- overflow: 'auto',
210
- ...BG_STYLES[bgMode],
211
- }}
220
+ className={`jogak:flex-1 jogak:overflow-auto jogak:min-h-[320px] ${CANVAS_BG_CLASS}`}
221
+ // eslint-disable-next-line no-restricted-syntax -- jogak: BG_VARS object inject
222
+ style={BG_VARS[bgMode]}
212
223
  >
213
224
  <div
214
- style={{
215
- maxWidth: maxWidth === 'none' ? '100%' : maxWidth,
216
- margin: '0 auto',
217
- padding: 24,
218
- }}
225
+ className="jogak:mx-auto jogak:p-6 jogak:max-w-[var(--jogak-canvas-mw)]"
226
+ // eslint-disable-next-line no-restricted-syntax -- jogak: canvas-mw CSS var
227
+ style={
228
+ {
229
+ '--jogak-canvas-mw': maxWidth === 'none' ? '100%' : `${maxWidth}px`,
230
+ } as CSSVarStyle
231
+ }
219
232
  >
220
- <div
221
- style={{
222
- border: '1px dashed #e5e7eb',
223
- borderRadius: 8,
224
- padding: 16,
225
- minHeight: CANVAS_MIN_HEIGHT - 64,
226
- display: 'flex',
227
- alignItems: 'center',
228
- justifyContent: 'center',
229
- color: '#9ca3af',
230
- fontSize: 13,
231
- background:
232
- 'linear-gradient(90deg, rgba(229,231,235,0) 0%, rgba(229,231,235,0.45) 50%, rgba(229,231,235,0) 100%)',
233
- backgroundSize: '200% 100%',
234
- animation: 'jogakSkeleton 1.4s ease-in-out infinite',
235
- }}
236
- >
233
+ {/*
234
+ * skeleton box — 알파.5 PR 4 마이그레이션: gradient + keyframe animation 을
235
+ * jogak.css `@layer components` 의 `.jogak-skeleton-shimmer` class 로 이동
236
+ * (api-contracts §6). inline `style={{...}}` 객체 + inline `<style>` 태그
237
+ * 동시 제거. 정적 부분(border / radius / padding / flex / color / fontSize /
238
+ * minHeight)은 jogak: utility 그대로 유지.
239
+ */}
240
+ <div className="jogak-skeleton-shimmer jogak:border jogak:border-dashed jogak:border-[var(--jogak-color-border)] jogak:rounded-[var(--jogak-radius-xl)] jogak:p-4 jogak:flex jogak:items-center jogak:justify-center jogak:text-[var(--jogak-color-fg-subtle)] jogak:text-[13px] jogak:min-h-[256px]">
237
241
  Loading {meta.title}…
238
242
  </div>
239
243
  </div>
240
244
  </div>
241
- <style>
242
- {`@keyframes jogakSkeleton { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } }`}
243
- </style>
244
245
  </div>
245
246
  )
246
247
  }
@@ -289,7 +290,7 @@ function ReadyFrame({
289
290
 
290
291
  if (resolvedJogakName === null) {
291
292
  return (
292
- <div style={{ padding: 24, color: '#ef4444' }}>
293
+ <div className="jogak:p-6 jogak:text-[var(--jogak-color-error)]">
293
294
  Entry has no jogaks: {entry.id}
294
295
  </div>
295
296
  )
@@ -298,7 +299,7 @@ function ReadyFrame({
298
299
  const jogak = entry.jogaks.find((j) => j.name === resolvedJogakName)
299
300
  if (jogak === undefined) {
300
301
  return (
301
- <div style={{ padding: 24, color: '#ef4444' }}>
302
+ <div className="jogak:p-6 jogak:text-[var(--jogak-color-error)]">
302
303
  Jogak not found: {resolvedJogakName}
303
304
  </div>
304
305
  )
@@ -314,7 +315,7 @@ function ReadyFrame({
314
315
  const maxWidth = VIEWPORT_WIDTHS[viewport]
315
316
 
316
317
  return (
317
- <div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
318
+ <div className="jogak:flex jogak:flex-col jogak:h-full">
318
319
  <Toolbar
319
320
  title={entry.title}
320
321
  jogakName={jogak.name}
@@ -328,20 +329,19 @@ function ReadyFrame({
328
329
 
329
330
  {/* ── 캔버스 ───────────────────────────────────────── */}
330
331
  <div
331
- style={{
332
- flex: 1,
333
- minHeight: CANVAS_MIN_HEIGHT,
334
- overflow: 'auto',
335
- ...BG_STYLES[bgMode],
336
- }}
332
+ className={`jogak:flex-1 jogak:overflow-auto jogak:min-h-[320px] ${CANVAS_BG_CLASS}`}
333
+ // eslint-disable-next-line no-restricted-syntax -- jogak: BG_VARS object inject
334
+ style={BG_VARS[bgMode]}
337
335
  >
338
336
  <div
339
337
  data-jogak-content
340
- style={{
341
- maxWidth: maxWidth === 'none' ? '100%' : maxWidth,
342
- margin: '0 auto',
343
- padding: 24,
344
- }}
338
+ className="jogak:mx-auto jogak:p-6 jogak:max-w-[var(--jogak-canvas-mw)]"
339
+ // eslint-disable-next-line no-restricted-syntax -- jogak: canvas-mw CSS var
340
+ style={
341
+ {
342
+ '--jogak-canvas-mw': maxWidth === 'none' ? '100%' : `${maxWidth}px`,
343
+ } as CSSVarStyle
344
+ }
345
345
  >
346
346
  <JogakRenderer
347
347
  key={`${entry.id}/${jogak.name}`}
@@ -355,24 +355,12 @@ function ReadyFrame({
355
355
 
356
356
  {/* ── 컨트롤/액션 패널 ──────────────────────────────── */}
357
357
  <div
358
- style={{
359
- height: 260,
360
- flexShrink: 0,
361
- display: 'flex',
362
- flexDirection: 'column',
363
- borderTop: '2px solid #e5e7eb',
364
- }}
358
+ data-testid="bottom-panel"
359
+ className="jogak:h-[260px] jogak:shrink-0 jogak:flex jogak:flex-col jogak:border-t-2 jogak:border-[var(--jogak-color-border)]"
365
360
  >
366
361
  <div
367
362
  role="tablist"
368
- style={{
369
- display: 'flex',
370
- gap: 4,
371
- padding: '4px 12px 0',
372
- background: '#fff',
373
- borderBottom: '1px solid #e5e7eb',
374
- flexShrink: 0,
375
- }}
363
+ className="jogak:flex jogak:gap-1 jogak:pt-1 jogak:px-3 jogak:pb-0 jogak:bg-[var(--jogak-color-bg)] jogak:border-b jogak:border-[var(--jogak-color-border)] jogak:shrink-0"
376
364
  >
377
365
  {(['controls', 'actions'] as const).map((tab) => {
378
366
  const active = bottomTab === tab
@@ -383,18 +371,12 @@ function ReadyFrame({
383
371
  role="tab"
384
372
  aria-selected={active}
385
373
  onClick={() => { onBottomTabChange(tab) }}
386
- style={{
387
- padding: '6px 14px',
388
- fontSize: 12,
389
- fontWeight: active ? 600 : 500,
390
- color: active ? '#111827' : '#6b7280',
391
- background: 'transparent',
392
- border: 'none',
393
- borderBottom: active ? '2px solid #2563eb' : '2px solid transparent',
394
- marginBottom: -1,
395
- cursor: 'pointer',
396
- textTransform: 'capitalize',
397
- }}
374
+ className={clsx(
375
+ 'jogak:px-[14px] jogak:py-[6px] jogak:text-[12px] jogak:bg-transparent jogak:border-x-0 jogak:border-t-0 jogak:border-b-2 jogak:border-solid jogak:-mb-px jogak:cursor-pointer jogak:capitalize',
376
+ active
377
+ ? 'jogak:font-semibold jogak:text-[var(--jogak-color-fg-strong)] jogak:border-[var(--jogak-color-accent)]'
378
+ : 'jogak:font-medium jogak:text-[var(--jogak-color-fg-muted)] jogak:border-transparent',
379
+ )}
398
380
  >
399
381
  {tab}
400
382
  </button>
@@ -402,7 +384,7 @@ function ReadyFrame({
402
384
  })}
403
385
  </div>
404
386
 
405
- <div style={{ flex: 1, minHeight: 0, overflow: 'auto' }}>
387
+ <div className="jogak:flex-1 jogak:min-h-0 jogak:overflow-auto">
406
388
  {bottomTab === 'controls' ? (
407
389
  <Controls
408
390
  args={mergedArgs}
@@ -442,51 +424,31 @@ function Toolbar({
442
424
  onReset,
443
425
  }: ToolbarProps): ReactElement {
444
426
  return (
445
- <div
446
- style={{
447
- display: 'flex',
448
- alignItems: 'center',
449
- gap: 10,
450
- padding: '7px 14px',
451
- borderBottom: '1px solid #e5e7eb',
452
- background: '#fff',
453
- flexShrink: 0,
454
- }}
455
- >
456
- <div style={{ flex: 1, fontSize: 13 }}>
457
- <span style={{ color: '#9ca3af' }}>{title}</span>
458
- <span style={{ color: '#d1d5db', margin: '0 6px' }}>/</span>
459
- <span style={{ color: '#111827', fontWeight: 600 }}>{jogakName}</span>
427
+ <div className="jogak:flex jogak:items-center jogak:gap-[10px] jogak:px-[14px] jogak:py-[7px] jogak:border-b jogak:border-[var(--jogak-color-border)] jogak:bg-[var(--jogak-color-bg)] jogak:shrink-0">
428
+ <div className="jogak:flex-1 jogak:text-[13px]">
429
+ <span className="jogak:text-[var(--jogak-color-fg-subtle)]">{title}</span>
430
+ <span className="jogak:text-[var(--jogak-color-border-strong)] jogak:mx-1.5 jogak:leading-none">
431
+ /
432
+ </span>
433
+ <span className="jogak:text-[var(--jogak-color-fg-strong)] jogak:font-semibold">
434
+ {jogakName}
435
+ </span>
460
436
  </div>
461
437
 
462
438
  {/* 뷰포트 토글 */}
463
- <div
464
- style={{
465
- display: 'flex',
466
- gap: 2,
467
- background: '#f3f4f6',
468
- borderRadius: 6,
469
- padding: 2,
470
- }}
471
- >
439
+ <div className="jogak:flex jogak:gap-0.5 jogak:bg-[var(--jogak-color-bg-subtle)] jogak:rounded-[var(--jogak-radius-lg)] jogak:p-0.5">
472
440
  {(['mobile', 'tablet', 'desktop'] as const).map((vp) => (
473
441
  <button
474
442
  key={vp}
475
443
  type="button"
476
444
  onClick={() => { onViewportChange(vp) }}
477
445
  aria-pressed={viewport === vp}
478
- style={{
479
- padding: '3px 9px',
480
- fontSize: 12,
481
- border: 'none',
482
- borderRadius: 4,
483
- cursor: 'pointer',
484
- background: viewport === vp ? '#fff' : 'transparent',
485
- color: viewport === vp ? '#111827' : '#6b7280',
486
- fontWeight: viewport === vp ? 600 : 400,
487
- boxShadow: viewport === vp ? '0 1px 2px rgba(0,0,0,0.08)' : 'none',
488
- transition: 'all 0.1s',
489
- }}
446
+ className={clsx(
447
+ 'jogak:px-[9px] jogak:py-[3px] jogak:text-[12px] jogak:border-none jogak:rounded-[var(--jogak-radius-md)] jogak:cursor-pointer jogak:transition-all jogak:duration-100',
448
+ viewport === vp
449
+ ? 'jogak:bg-[var(--jogak-color-bg-elevated)] jogak:text-[var(--jogak-color-fg-strong)] jogak:font-semibold jogak:shadow-[0_1px_2px_rgba(0,0,0,0.08)]'
450
+ : 'jogak:bg-transparent jogak:text-[var(--jogak-color-fg-muted)] jogak:font-normal jogak:shadow-none',
451
+ )}
490
452
  >
491
453
  {VIEWPORT_LABELS[vp]}
492
454
  </button>
@@ -494,7 +456,7 @@ function Toolbar({
494
456
  </div>
495
457
 
496
458
  {/* 배경 토글 */}
497
- <div style={{ display: 'flex', gap: 4, alignItems: 'center' }}>
459
+ <div className="jogak:flex jogak:gap-1 jogak:items-center">
498
460
  {(['white', 'dark', 'transparent'] as const).map((bg) => (
499
461
  <button
500
462
  key={bg}
@@ -502,16 +464,15 @@ function Toolbar({
502
464
  onClick={() => { onBgModeChange(bg) }}
503
465
  aria-pressed={bgMode === bg}
504
466
  aria-label={`${bg} background`}
505
- style={{
506
- width: 20,
507
- height: 20,
508
- borderRadius: 4,
509
- border: bgMode === bg ? '2px solid #2563eb' : '2px solid #d1d5db',
510
- cursor: 'pointer',
511
- padding: 0,
512
- flexShrink: 0,
513
- ...BG_STYLES[bg],
514
- }}
467
+ className={clsx(
468
+ 'jogak:w-5 jogak:h-5 jogak:rounded-[var(--jogak-radius-md)] jogak:border-2 jogak:cursor-pointer jogak:p-0 jogak:shrink-0',
469
+ CANVAS_BG_CLASS,
470
+ bgMode === bg
471
+ ? 'jogak:border-[var(--jogak-color-accent)]'
472
+ : 'jogak:border-[var(--jogak-color-border-strong)]',
473
+ )}
474
+ // eslint-disable-next-line no-restricted-syntax -- jogak: BG_VARS object inject (3 mini buttons)
475
+ style={BG_VARS[bg]}
515
476
  />
516
477
  ))}
517
478
  </div>
@@ -521,15 +482,7 @@ function Toolbar({
521
482
  <button
522
483
  type="button"
523
484
  onClick={onReset}
524
- style={{
525
- padding: '3px 10px',
526
- fontSize: 12,
527
- border: '1px solid #d1d5db',
528
- borderRadius: 4,
529
- background: '#fff',
530
- cursor: 'pointer',
531
- color: '#374151',
532
- }}
485
+ className="jogak:px-[10px] jogak:py-[3px] jogak:text-[12px] jogak:border jogak:border-[var(--jogak-color-border-strong)] jogak:rounded-[var(--jogak-radius-md)] jogak:bg-[var(--jogak-color-bg)] jogak:cursor-pointer jogak:text-[var(--jogak-color-fg)] jogak:leading-none"
533
486
  >
534
487
  Reset
535
488
  </button>
@@ -568,40 +521,24 @@ function JogakRenderer({ entry, args, source, theme }: JogakRendererProps): Reac
568
521
  return (
569
522
  <div>
570
523
  {/* preview-content 영역 + 토글 버튼 */}
571
- <div style={{ position: 'relative' }}>
524
+ <div className="jogak:relative">
572
525
  <div
573
526
  ref={containerRef}
574
527
  data-testid="preview-content"
575
- style={{
576
- border: '1px dashed #e5e7eb',
577
- borderRadius: 8,
578
- padding: 16,
579
- paddingBottom: 36,
580
- }}
528
+ className="jogak:border jogak:border-dashed jogak:border-[var(--jogak-color-border)] jogak:rounded-[var(--jogak-radius-xl)] jogak:p-4 jogak:pb-9"
581
529
  />
582
530
  <button
583
531
  type="button"
584
532
  onClick={() => { setShowCode((v) => !v) }}
585
533
  aria-pressed={showCode}
586
534
  aria-label={showCode ? 'Hide source code' : 'Show source code'}
587
- style={{
588
- position: 'absolute',
589
- bottom: 8,
590
- right: 8,
591
- padding: '4px 9px',
592
- fontSize: 11,
593
- fontFamily:
594
- 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace',
595
- fontWeight: 600,
596
- letterSpacing: '0.02em',
597
- background: showCode ? '#2563eb' : '#1e293b',
598
- color: '#fff',
599
- border: 'none',
600
- borderRadius: 5,
601
- cursor: 'pointer',
602
- boxShadow: '0 1px 4px rgba(0,0,0,0.2)',
603
- transition: 'background 0.15s',
604
- }}
535
+ className={clsx(
536
+ 'jogak:absolute jogak:bottom-2 jogak:right-2 jogak:px-[9px] jogak:py-1',
537
+ 'jogak:text-[11px] jogak:font-[family-name:var(--jogak-font-mono)] jogak:font-semibold jogak:tracking-[0.02em]',
538
+ 'jogak:text-[var(--jogak-color-bg)] jogak:border-none jogak:rounded-[5px] jogak:cursor-pointer',
539
+ 'jogak:shadow-[0_1px_4px_rgba(0,0,0,0.2)] jogak:transition-[background-color] jogak:duration-150 jogak:leading-none',
540
+ showCode ? 'jogak:bg-[var(--jogak-color-accent)]' : 'jogak:bg-[#1e293b]',
541
+ )}
605
542
  >
606
543
  {'</>'}
607
544
  </button>
@@ -609,15 +546,7 @@ function JogakRenderer({ entry, args, source, theme }: JogakRendererProps): Reac
609
546
 
610
547
  {/* 코드 패널 — preview-content 하단으로 펼쳐짐 */}
611
548
  {showCode && (
612
- <div
613
- style={{
614
- marginTop: 8,
615
- borderRadius: 8,
616
- overflow: 'hidden',
617
- height: 320,
618
- boxShadow: '0 0 0 1px rgba(0,0,0,0.08), 0 4px 16px rgba(0,0,0,0.12)',
619
- }}
620
- >
549
+ <div className="jogak:mt-2 jogak:rounded-[var(--jogak-radius-xl)] jogak:overflow-hidden jogak:h-[320px] jogak:shadow-[0_0_0_1px_rgba(0,0,0,0.08),_0_4px_16px_rgba(0,0,0,0.12)]">
621
550
  <SourceViewer source={source} theme={theme} />
622
551
  </div>
623
552
  )}
@@ -639,15 +568,9 @@ function SourceViewer({ source, theme }: SourceViewerProps): ReactElement {
639
568
  if (source === undefined) {
640
569
  return (
641
570
  <div
642
- style={{
643
- height: '100%',
644
- display: 'flex',
645
- alignItems: 'center',
646
- justifyContent: 'center',
647
- background: bgColor,
648
- color: '#94a3b8',
649
- fontSize: 13,
650
- }}
571
+ className="jogak:h-full jogak:flex jogak:items-center jogak:justify-center jogak:bg-[var(--jogak-source-bg)] jogak:text-[#94a3b8] jogak:text-[13px]"
572
+ // eslint-disable-next-line no-restricted-syntax -- jogak: source-bg CSS var (prism theme)
573
+ style={{ '--jogak-source-bg': bgColor } as CSSVarStyle}
651
574
  >
652
575
  Source not available
653
576
  </div>
@@ -662,23 +585,11 @@ function SourceViewer({ source, theme }: SourceViewerProps): ReactElement {
662
585
  }
663
586
 
664
587
  return (
665
- <div style={{ position: 'relative', height: '100%' }}>
588
+ <div className="jogak:relative jogak:h-full">
666
589
  <button
667
590
  type="button"
668
591
  onClick={handleCopy}
669
- style={{
670
- position: 'absolute',
671
- top: 10,
672
- right: 12,
673
- zIndex: 1,
674
- padding: '3px 9px',
675
- fontSize: 11,
676
- background: 'rgba(255,255,255,0.1)',
677
- color: '#e2e8f0',
678
- border: '1px solid rgba(255,255,255,0.18)',
679
- borderRadius: 4,
680
- cursor: 'pointer',
681
- }}
592
+ className="jogak:absolute jogak:top-[10px] jogak:right-3 jogak:z-[1] jogak:px-[9px] jogak:py-[3px] jogak:text-[11px] jogak:bg-[rgba(255,255,255,0.1)] jogak:text-[#e2e8f0] jogak:border jogak:border-[rgba(255,255,255,0.18)] jogak:rounded-[var(--jogak-radius-md)] jogak:cursor-pointer jogak:leading-none"
682
593
  >
683
594
  {copied ? '✓ Copied' : 'Copy'}
684
595
  </button>
@@ -686,40 +597,19 @@ function SourceViewer({ source, theme }: SourceViewerProps): ReactElement {
686
597
  <Highlight code={source.trim()} language="tsx" theme={theme}>
687
598
  {({ style, tokens, getLineProps, getTokenProps }) => (
688
599
  <pre
689
- style={{
690
- ...style,
691
- margin: 0,
692
- padding: '12px 0',
693
- fontSize: 12.5,
694
- lineHeight: 1.7,
695
- fontFamily:
696
- 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace',
697
- height: '100%',
698
- boxSizing: 'border-box',
699
- overflow: 'auto',
700
- }}
600
+ className="jogak:m-0 jogak:py-3 jogak:px-0 jogak:text-[12.5px] jogak:leading-[1.7] jogak:font-[family-name:var(--jogak-font-mono)] jogak:h-full jogak:box-border jogak:overflow-auto"
601
+ // eslint-disable-next-line no-restricted-syntax -- jogak: prism-react-renderer external interface (pre)
602
+ style={style}
701
603
  >
702
604
  {tokens.map((line, i) => (
703
605
  <div
704
606
  key={i}
705
607
  {...getLineProps({ line })}
706
- style={{
707
- ...getLineProps({ line }).style,
708
- display: 'flex',
709
- paddingRight: 24,
710
- }}
608
+ className="jogak:flex jogak:pr-6"
609
+ // eslint-disable-next-line no-restricted-syntax -- jogak: prism-react-renderer external interface (line)
610
+ style={getLineProps({ line }).style}
711
611
  >
712
- <span
713
- style={{
714
- userSelect: 'none',
715
- minWidth: 40,
716
- paddingLeft: 14,
717
- paddingRight: 14,
718
- textAlign: 'right',
719
- color: 'rgba(148,163,184,0.45)',
720
- flexShrink: 0,
721
- }}
722
- >
612
+ <span className="jogak:select-none jogak:min-w-10 jogak:pl-[14px] jogak:pr-[14px] jogak:text-right jogak:text-[rgba(148,163,184,0.45)] jogak:shrink-0 jogak:leading-[1.7]">
723
613
  {i + 1}
724
614
  </span>
725
615
  <span>