@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,8 +1,14 @@
1
1
  import { useState, useEffect } from 'react'
2
- import type { ReactElement } from 'react'
2
+ import type { CSSProperties, ReactElement } from 'react'
3
+ import clsx from 'clsx'
3
4
  import type { CategoryMetaTree, RegistryEntryMeta } from '@jogak/core'
4
5
  import { useRegistryMeta } from '@jogak/react'
5
6
 
7
+ // CSS custom property를 React style prop에 주입하기 위한 헬퍼 타입.
8
+ // React 18+는 string-keyed `--` prefix를 인식하나 TS는 명시적 cast 필요.
9
+ // spec §6.1 참조.
10
+ type CSSVarStyle = CSSProperties & Record<`--${string}`, string | number>
11
+
6
12
  export interface SidebarProps {
7
13
  readonly selectedEntryId: string | null
8
14
  readonly selectedJogakName: string | null
@@ -26,15 +32,9 @@ export function Sidebar({
26
32
  return (
27
33
  <aside
28
34
  data-testid="sidebar"
29
- style={{
30
- borderRight: '1px solid #e5e7eb',
31
- height: '100%',
32
- overflow: 'auto',
33
- display: 'flex',
34
- flexDirection: 'column',
35
- }}
35
+ className="jogak:flex jogak:flex-col jogak:h-full jogak:overflow-auto jogak:border-r jogak:border-[var(--jogak-color-border)]"
36
36
  >
37
- <div style={{ padding: '12px', borderBottom: '1px solid #e5e7eb' }}>
37
+ <div className="jogak:p-3 jogak:border-b jogak:border-[var(--jogak-color-border)]">
38
38
  <input
39
39
  type="search"
40
40
  placeholder="Search components..."
@@ -42,16 +42,11 @@ export function Sidebar({
42
42
  onChange={(e) => {
43
43
  setQuery(e.target.value)
44
44
  }}
45
- style={{
46
- width: '100%',
47
- padding: '6px 8px',
48
- border: '1px solid #d1d5db',
49
- borderRadius: 4,
50
- }}
45
+ className="jogak:w-full jogak:px-2 jogak:py-1.5 jogak:border jogak:border-[var(--jogak-color-border-strong)] jogak:rounded-[var(--jogak-radius-md)]"
51
46
  aria-label="Search components"
52
47
  />
53
48
  </div>
54
- <nav style={{ flex: 1, overflow: 'auto', padding: '8px 0' }}>
49
+ <nav className="jogak:flex-1 jogak:overflow-auto jogak:py-2">
55
50
  {filtered !== null ? (
56
51
  <FlatList
57
52
  metas={filtered}
@@ -87,13 +82,13 @@ function FlatList({
87
82
  }: FlatListProps): ReactElement {
88
83
  if (metas.length === 0) {
89
84
  return (
90
- <p style={{ padding: '0 12px', color: '#9ca3af', fontSize: 13 }}>
85
+ <p className="jogak:px-3 jogak:text-[var(--jogak-color-fg-subtle)] jogak:text-[13px]">
91
86
  No results
92
87
  </p>
93
88
  )
94
89
  }
95
90
  return (
96
- <ul style={{ listStyle: 'none', margin: 0, padding: 0 }}>
91
+ <ul className="jogak:list-none jogak:m-0 jogak:p-0">
97
92
  {metas.map((meta) => (
98
93
  <li key={meta.id}>
99
94
  <EntryGroup
@@ -126,11 +121,9 @@ function TreeView({
126
121
  }: TreeViewProps): ReactElement {
127
122
  return (
128
123
  <ul
129
- style={{
130
- listStyle: 'none',
131
- margin: 0,
132
- padding: `0 0 0 ${depth * 12}px`,
133
- }}
124
+ className="jogak:list-none jogak:m-0 jogak:pr-0 jogak:py-0 jogak:pl-[var(--jogak-tree-pl)]"
125
+ // eslint-disable-next-line no-restricted-syntax -- jogak: CSS var inject (--jogak-tree-pl)
126
+ style={{ '--jogak-tree-pl': `${depth * 12}px` } as CSSVarStyle}
134
127
  >
135
128
  {Object.entries(node).map(([key, child]) => (
136
129
  <li key={key}>
@@ -183,21 +176,7 @@ function CategoryGroup({
183
176
  onClick={() => {
184
177
  setOpen((v) => !v)
185
178
  }}
186
- style={{
187
- display: 'flex',
188
- alignItems: 'center',
189
- gap: 4,
190
- width: '100%',
191
- padding: '4px 12px',
192
- background: 'none',
193
- border: 'none',
194
- cursor: 'pointer',
195
- fontSize: 12,
196
- fontWeight: 600,
197
- color: '#6b7280',
198
- textTransform: 'uppercase',
199
- letterSpacing: '0.05em',
200
- }}
179
+ className="jogak:flex jogak:items-center jogak:gap-1 jogak:w-full jogak:px-3 jogak:py-1 jogak:bg-transparent jogak:border-none jogak:cursor-pointer jogak:text-[12px] jogak:font-semibold jogak:text-[var(--jogak-color-fg-muted)] jogak:uppercase jogak:tracking-wider"
201
180
  aria-expanded={open}
202
181
  >
203
182
  <span>{open ? '▾' : '▸'}</span>
@@ -254,29 +233,25 @@ function EntryGroup({
254
233
  setOpen((v) => !v)
255
234
  }
256
235
  }}
257
- style={{
258
- display: 'flex',
259
- alignItems: 'center',
260
- gap: 6,
261
- width: '100%',
262
- padding: `5px 12px 5px ${paddingLeft}px`,
263
- background: isCurrentEntry ? '#eff6ff' : 'none',
264
- border: 'none',
265
- cursor: 'pointer',
266
- fontSize: 13,
267
- color: isCurrentEntry ? '#2563eb' : '#374151',
268
- fontWeight: isCurrentEntry ? 500 : 400,
269
- textAlign: 'left',
270
- }}
236
+ className={clsx(
237
+ 'jogak:flex jogak:items-center jogak:gap-1.5 jogak:w-full jogak:pr-3 jogak:py-[5px]',
238
+ 'jogak:pl-[var(--jogak-entry-pl)]',
239
+ 'jogak:border-none jogak:cursor-pointer jogak:text-left jogak:text-[13px]',
240
+ isCurrentEntry
241
+ ? 'jogak:bg-[var(--jogak-color-accent-bg)] jogak:text-[var(--jogak-color-accent)] jogak:font-medium'
242
+ : 'jogak:bg-transparent jogak:text-[var(--jogak-color-fg)] jogak:font-normal',
243
+ )}
244
+ // eslint-disable-next-line no-restricted-syntax -- jogak: CSS var inject (--jogak-entry-pl)
245
+ style={{ '--jogak-entry-pl': `${paddingLeft}px` } as CSSVarStyle}
271
246
  aria-expanded={open}
272
247
  >
273
- <span style={{ fontSize: 10, flexShrink: 0, lineHeight: 1 }}>
248
+ <span className="jogak:text-[10px] jogak:shrink-0 jogak:leading-none">
274
249
  {open ? '▾' : '▸'}
275
250
  </span>
276
251
  {label}
277
252
  </button>
278
253
  {open && (
279
- <ul style={{ listStyle: 'none', margin: 0, padding: 0 }}>
254
+ <ul className="jogak:list-none jogak:m-0 jogak:p-0">
280
255
  {meta.jogakNames.map((jogakName) => {
281
256
  const isSelected = isCurrentEntry && jogakName === selectedJogakName
282
257
  return (
@@ -286,18 +261,16 @@ function EntryGroup({
286
261
  onClick={() => {
287
262
  onSelect(meta.id, jogakName)
288
263
  }}
289
- style={{
290
- display: 'block',
291
- width: '100%',
292
- textAlign: 'left',
293
- padding: `4px 12px 4px ${paddingLeft + 18}px`,
294
- background: isSelected ? '#dbeafe' : 'none',
295
- border: 'none',
296
- cursor: 'pointer',
297
- fontSize: 12,
298
- color: isSelected ? '#1d4ed8' : '#6b7280',
299
- fontWeight: isSelected ? 500 : 400,
300
- }}
264
+ className={clsx(
265
+ 'jogak:block jogak:w-full jogak:text-left jogak:pr-3 jogak:py-1',
266
+ 'jogak:pl-[var(--jogak-jogak-pl)]',
267
+ 'jogak:border-none jogak:cursor-pointer jogak:text-[12px]',
268
+ isSelected
269
+ ? 'jogak:bg-[var(--jogak-color-accent-bg-soft)] jogak:text-[var(--jogak-color-accent-fg)] jogak:font-medium'
270
+ : 'jogak:bg-transparent jogak:text-[var(--jogak-color-fg-muted)] jogak:font-normal',
271
+ )}
272
+ // eslint-disable-next-line no-restricted-syntax -- jogak: CSS var inject (--jogak-jogak-pl)
273
+ style={{ '--jogak-jogak-pl': `${paddingLeft + 18}px` } as CSSVarStyle}
301
274
  aria-current={isSelected ? 'true' : undefined}
302
275
  >
303
276
  {jogakName}
@@ -0,0 +1,108 @@
1
+ /*
2
+ * jogak SPA 전용 스타일.
3
+ *
4
+ * 본 파일은 `packages/ui/src/app/main.tsx`에서만 import된다.
5
+ * lib mode build entry(`src/index.ts`, `src/host/index.ts`)에는 import되지 않으므로
6
+ * publish되는 `dist/index.{mjs,js}` / `dist/host/index.{mjs,js}` 산출물에 css 흔적이
7
+ * 들어가지 않는다 (사용자 번들 영향 zero — api-contracts §2.4, §7).
8
+ *
9
+ * Tailwind v4: preflight를 의도적으로 import하지 않고 `theme.css` + `utilities.css`만
10
+ * 분리 import한다. preflight의 globals(`*`, `html`, `body`)는 사용자 css와 prefix
11
+ * 무관하게 충돌 가능 — jogak SPA는 `index.html`의 inline reset(`box-sizing`,
12
+ * `body margin:0`)으로 충분하다 (api-contracts §6.2).
13
+ *
14
+ * `prefix(jogak)` — 모든 utility/variant/theme variable에 `jogak:` prefix가 적용된다.
15
+ * 사용자가 v4(no prefix)나 v3(`tw-` prefix)를 써도 selector 충돌 zero.
16
+ */
17
+
18
+ @import "tailwindcss/theme.css" layer(theme) prefix(jogak);
19
+ @import "tailwindcss/utilities.css" layer(utilities) prefix(jogak);
20
+
21
+ /*
22
+ * jogak 디자인 토큰 (`--jogak-*`).
23
+ *
24
+ * 정의 위치는 `:root` — preview 내부의 사용자 컴포넌트도 var() 참조 가능 (원하면).
25
+ * `--jogak-*` prefix가 충분한 namespace를 제공하므로 사용자 globalCss(알파.6 예정)
26
+ * 와 충돌 zero.
27
+ *
28
+ * 알파.4에서는 변수만 정의하고 컴포넌트는 inline style을 그대로 둔다.
29
+ * 알파.5에서 `style={{ color: '#374151' }}` → `className="jogak:text-[var(--jogak-color-fg)]"`
30
+ * 형태로 마이그레이션한다 (api-contracts §4.4).
31
+ */
32
+ @layer base {
33
+ :root {
34
+ /* color */
35
+ --jogak-color-bg: #ffffff;
36
+ --jogak-color-fg: #374151;
37
+ --jogak-color-fg-muted: #6b7280;
38
+ --jogak-color-fg-subtle: #9ca3af;
39
+ --jogak-color-fg-strong: #111827;
40
+ --jogak-color-bg-subtle: #f9fafb;
41
+ --jogak-color-bg-elevated: #ffffff;
42
+ --jogak-color-bg-error: #fef2f2;
43
+
44
+ --jogak-color-accent: #2563eb;
45
+ --jogak-color-accent-bg: #eff6ff;
46
+ --jogak-color-accent-fg: #1d4ed8;
47
+ --jogak-color-accent-bg-soft: #dbeafe;
48
+
49
+ --jogak-color-violet: #7c3aed;
50
+ --jogak-color-violet-bg: #f5f3ff;
51
+ --jogak-color-violet-border:#ddd6fe;
52
+
53
+ --jogak-color-error: #ef4444;
54
+ --jogak-color-error-fg: #b91c1c;
55
+ --jogak-color-error-border: #fecaca;
56
+
57
+ --jogak-color-border: #e5e7eb;
58
+ --jogak-color-border-muted: #f3f4f6;
59
+ --jogak-color-border-strong:#d1d5db;
60
+ --jogak-color-checker: #e2e8f0;
61
+
62
+ /* spacing */
63
+ --jogak-space-1: 4px;
64
+ --jogak-space-2: 8px;
65
+ --jogak-space-3: 12px;
66
+ --jogak-space-4: 16px;
67
+ --jogak-space-5: 20px;
68
+ --jogak-space-6: 24px;
69
+
70
+ /* radius */
71
+ --jogak-radius-sm: 3px;
72
+ --jogak-radius-md: 4px;
73
+ --jogak-radius-lg: 6px;
74
+ --jogak-radius-xl: 8px;
75
+
76
+ /* typography */
77
+ --jogak-font-sans: system-ui, sans-serif;
78
+ --jogak-font-mono: ui-monospace, SFMono-Regular, Menlo, monospace;
79
+ /*
80
+ * NOTE 알파.5 PR 4: --jogak-text-{xs,sm,base,md,lg} 5개 변수 + --jogak-sidebar-width
81
+ * 1개 삭제. 사용처 zero (PR 1/3에서 픽셀 literal 채택). v4 함정 1 (font-size 페어링)
82
+ * 회귀 자체 차단 + 알파.6 사용자 globalCss 충돌 가능성 zero.
83
+ */
84
+ }
85
+ }
86
+
87
+ @layer components {
88
+ /*
89
+ * 알파.5 PR 4: skeleton 애니메이션을 inline <style>(Preview L250-252)에서 jogak.css로
90
+ * 이동. inline style forbid rule (no-restricted-syntax) 위반 1건 + inline <style> 1건
91
+ * 동시 제거. VR 영향 zero (선택자/타이밍/그라디언트 픽셀 동일).
92
+ */
93
+ @keyframes jogakSkeleton {
94
+ 0% { background-position: 200% 0; }
95
+ 100% { background-position: -200% 0; }
96
+ }
97
+
98
+ .jogak-skeleton-shimmer {
99
+ background: linear-gradient(
100
+ 90deg,
101
+ rgba(229, 231, 235, 0) 0%,
102
+ rgba(229, 231, 235, 0.45) 50%,
103
+ rgba(229, 231, 235, 0) 100%
104
+ );
105
+ background-size: 200% 100%;
106
+ animation: jogakSkeleton 1.4s ease-in-out infinite;
107
+ }
108
+ }