@jogak/ui 0.1.0-alpha.3 → 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,19 +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
- style={{
340
- maxWidth: maxWidth === 'none' ? '100%' : maxWidth,
341
- margin: '0 auto',
342
- padding: 24,
343
- }}
337
+ data-jogak-content
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
+ }
344
345
  >
345
346
  <JogakRenderer
346
347
  key={`${entry.id}/${jogak.name}`}
@@ -354,24 +355,12 @@ function ReadyFrame({
354
355
 
355
356
  {/* ── 컨트롤/액션 패널 ──────────────────────────────── */}
356
357
  <div
357
- style={{
358
- height: 260,
359
- flexShrink: 0,
360
- display: 'flex',
361
- flexDirection: 'column',
362
- borderTop: '2px solid #e5e7eb',
363
- }}
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)]"
364
360
  >
365
361
  <div
366
362
  role="tablist"
367
- style={{
368
- display: 'flex',
369
- gap: 4,
370
- padding: '4px 12px 0',
371
- background: '#fff',
372
- borderBottom: '1px solid #e5e7eb',
373
- flexShrink: 0,
374
- }}
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"
375
364
  >
376
365
  {(['controls', 'actions'] as const).map((tab) => {
377
366
  const active = bottomTab === tab
@@ -382,18 +371,12 @@ function ReadyFrame({
382
371
  role="tab"
383
372
  aria-selected={active}
384
373
  onClick={() => { onBottomTabChange(tab) }}
385
- style={{
386
- padding: '6px 14px',
387
- fontSize: 12,
388
- fontWeight: active ? 600 : 500,
389
- color: active ? '#111827' : '#6b7280',
390
- background: 'transparent',
391
- border: 'none',
392
- borderBottom: active ? '2px solid #2563eb' : '2px solid transparent',
393
- marginBottom: -1,
394
- cursor: 'pointer',
395
- textTransform: 'capitalize',
396
- }}
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
+ )}
397
380
  >
398
381
  {tab}
399
382
  </button>
@@ -401,7 +384,7 @@ function ReadyFrame({
401
384
  })}
402
385
  </div>
403
386
 
404
- <div style={{ flex: 1, minHeight: 0, overflow: 'auto' }}>
387
+ <div className="jogak:flex-1 jogak:min-h-0 jogak:overflow-auto">
405
388
  {bottomTab === 'controls' ? (
406
389
  <Controls
407
390
  args={mergedArgs}
@@ -441,51 +424,31 @@ function Toolbar({
441
424
  onReset,
442
425
  }: ToolbarProps): ReactElement {
443
426
  return (
444
- <div
445
- style={{
446
- display: 'flex',
447
- alignItems: 'center',
448
- gap: 10,
449
- padding: '7px 14px',
450
- borderBottom: '1px solid #e5e7eb',
451
- background: '#fff',
452
- flexShrink: 0,
453
- }}
454
- >
455
- <div style={{ flex: 1, fontSize: 13 }}>
456
- <span style={{ color: '#9ca3af' }}>{title}</span>
457
- <span style={{ color: '#d1d5db', margin: '0 6px' }}>/</span>
458
- <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>
459
436
  </div>
460
437
 
461
438
  {/* 뷰포트 토글 */}
462
- <div
463
- style={{
464
- display: 'flex',
465
- gap: 2,
466
- background: '#f3f4f6',
467
- borderRadius: 6,
468
- padding: 2,
469
- }}
470
- >
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">
471
440
  {(['mobile', 'tablet', 'desktop'] as const).map((vp) => (
472
441
  <button
473
442
  key={vp}
474
443
  type="button"
475
444
  onClick={() => { onViewportChange(vp) }}
476
445
  aria-pressed={viewport === vp}
477
- style={{
478
- padding: '3px 9px',
479
- fontSize: 12,
480
- border: 'none',
481
- borderRadius: 4,
482
- cursor: 'pointer',
483
- background: viewport === vp ? '#fff' : 'transparent',
484
- color: viewport === vp ? '#111827' : '#6b7280',
485
- fontWeight: viewport === vp ? 600 : 400,
486
- boxShadow: viewport === vp ? '0 1px 2px rgba(0,0,0,0.08)' : 'none',
487
- transition: 'all 0.1s',
488
- }}
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
+ )}
489
452
  >
490
453
  {VIEWPORT_LABELS[vp]}
491
454
  </button>
@@ -493,7 +456,7 @@ function Toolbar({
493
456
  </div>
494
457
 
495
458
  {/* 배경 토글 */}
496
- <div style={{ display: 'flex', gap: 4, alignItems: 'center' }}>
459
+ <div className="jogak:flex jogak:gap-1 jogak:items-center">
497
460
  {(['white', 'dark', 'transparent'] as const).map((bg) => (
498
461
  <button
499
462
  key={bg}
@@ -501,16 +464,15 @@ function Toolbar({
501
464
  onClick={() => { onBgModeChange(bg) }}
502
465
  aria-pressed={bgMode === bg}
503
466
  aria-label={`${bg} background`}
504
- style={{
505
- width: 20,
506
- height: 20,
507
- borderRadius: 4,
508
- border: bgMode === bg ? '2px solid #2563eb' : '2px solid #d1d5db',
509
- cursor: 'pointer',
510
- padding: 0,
511
- flexShrink: 0,
512
- ...BG_STYLES[bg],
513
- }}
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]}
514
476
  />
515
477
  ))}
516
478
  </div>
@@ -520,15 +482,7 @@ function Toolbar({
520
482
  <button
521
483
  type="button"
522
484
  onClick={onReset}
523
- style={{
524
- padding: '3px 10px',
525
- fontSize: 12,
526
- border: '1px solid #d1d5db',
527
- borderRadius: 4,
528
- background: '#fff',
529
- cursor: 'pointer',
530
- color: '#374151',
531
- }}
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"
532
486
  >
533
487
  Reset
534
488
  </button>
@@ -567,40 +521,24 @@ function JogakRenderer({ entry, args, source, theme }: JogakRendererProps): Reac
567
521
  return (
568
522
  <div>
569
523
  {/* preview-content 영역 + 토글 버튼 */}
570
- <div style={{ position: 'relative' }}>
524
+ <div className="jogak:relative">
571
525
  <div
572
526
  ref={containerRef}
573
527
  data-testid="preview-content"
574
- style={{
575
- border: '1px dashed #e5e7eb',
576
- borderRadius: 8,
577
- padding: 16,
578
- paddingBottom: 36,
579
- }}
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"
580
529
  />
581
530
  <button
582
531
  type="button"
583
532
  onClick={() => { setShowCode((v) => !v) }}
584
533
  aria-pressed={showCode}
585
534
  aria-label={showCode ? 'Hide source code' : 'Show source code'}
586
- style={{
587
- position: 'absolute',
588
- bottom: 8,
589
- right: 8,
590
- padding: '4px 9px',
591
- fontSize: 11,
592
- fontFamily:
593
- 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace',
594
- fontWeight: 600,
595
- letterSpacing: '0.02em',
596
- background: showCode ? '#2563eb' : '#1e293b',
597
- color: '#fff',
598
- border: 'none',
599
- borderRadius: 5,
600
- cursor: 'pointer',
601
- boxShadow: '0 1px 4px rgba(0,0,0,0.2)',
602
- transition: 'background 0.15s',
603
- }}
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
+ )}
604
542
  >
605
543
  {'</>'}
606
544
  </button>
@@ -608,15 +546,7 @@ function JogakRenderer({ entry, args, source, theme }: JogakRendererProps): Reac
608
546
 
609
547
  {/* 코드 패널 — preview-content 하단으로 펼쳐짐 */}
610
548
  {showCode && (
611
- <div
612
- style={{
613
- marginTop: 8,
614
- borderRadius: 8,
615
- overflow: 'hidden',
616
- height: 320,
617
- boxShadow: '0 0 0 1px rgba(0,0,0,0.08), 0 4px 16px rgba(0,0,0,0.12)',
618
- }}
619
- >
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)]">
620
550
  <SourceViewer source={source} theme={theme} />
621
551
  </div>
622
552
  )}
@@ -638,15 +568,9 @@ function SourceViewer({ source, theme }: SourceViewerProps): ReactElement {
638
568
  if (source === undefined) {
639
569
  return (
640
570
  <div
641
- style={{
642
- height: '100%',
643
- display: 'flex',
644
- alignItems: 'center',
645
- justifyContent: 'center',
646
- background: bgColor,
647
- color: '#94a3b8',
648
- fontSize: 13,
649
- }}
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}
650
574
  >
651
575
  Source not available
652
576
  </div>
@@ -661,23 +585,11 @@ function SourceViewer({ source, theme }: SourceViewerProps): ReactElement {
661
585
  }
662
586
 
663
587
  return (
664
- <div style={{ position: 'relative', height: '100%' }}>
588
+ <div className="jogak:relative jogak:h-full">
665
589
  <button
666
590
  type="button"
667
591
  onClick={handleCopy}
668
- style={{
669
- position: 'absolute',
670
- top: 10,
671
- right: 12,
672
- zIndex: 1,
673
- padding: '3px 9px',
674
- fontSize: 11,
675
- background: 'rgba(255,255,255,0.1)',
676
- color: '#e2e8f0',
677
- border: '1px solid rgba(255,255,255,0.18)',
678
- borderRadius: 4,
679
- cursor: 'pointer',
680
- }}
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"
681
593
  >
682
594
  {copied ? '✓ Copied' : 'Copy'}
683
595
  </button>
@@ -685,40 +597,19 @@ function SourceViewer({ source, theme }: SourceViewerProps): ReactElement {
685
597
  <Highlight code={source.trim()} language="tsx" theme={theme}>
686
598
  {({ style, tokens, getLineProps, getTokenProps }) => (
687
599
  <pre
688
- style={{
689
- ...style,
690
- margin: 0,
691
- padding: '12px 0',
692
- fontSize: 12.5,
693
- lineHeight: 1.7,
694
- fontFamily:
695
- 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace',
696
- height: '100%',
697
- boxSizing: 'border-box',
698
- overflow: 'auto',
699
- }}
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}
700
603
  >
701
604
  {tokens.map((line, i) => (
702
605
  <div
703
606
  key={i}
704
607
  {...getLineProps({ line })}
705
- style={{
706
- ...getLineProps({ line }).style,
707
- display: 'flex',
708
- paddingRight: 24,
709
- }}
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}
710
611
  >
711
- <span
712
- style={{
713
- userSelect: 'none',
714
- minWidth: 40,
715
- paddingLeft: 14,
716
- paddingRight: 14,
717
- textAlign: 'right',
718
- color: 'rgba(148,163,184,0.45)',
719
- flexShrink: 0,
720
- }}
721
- >
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]">
722
613
  {i + 1}
723
614
  </span>
724
615
  <span>