@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.
- package/CHANGELOG.md +51 -0
- package/README.md +15 -0
- package/dist/host/index.js +1 -1
- package/dist/host/index.mjs +47 -47
- package/dist/index.js +1 -1
- package/dist/index.mjs +597 -912
- package/package.json +7 -3
- package/src/app/App.tsx +4 -16
- package/src/app/main.tsx +1 -0
- package/src/components/Actions/index.tsx +20 -50
- package/src/components/Controls/index.tsx +49 -70
- package/src/components/Preview/index.tsx +141 -250
- package/src/components/Sidebar/index.tsx +39 -66
- package/src/styles/jogak.css +108 -0
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
|
|
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
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
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
|
+
}
|