@xfilecom/xframe 0.1.33 → 0.1.34

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/defaults.json CHANGED
@@ -1,4 +1,4 @@
1
1
  {
2
- "backendCore": "^1.0.13",
3
- "frontCore": "^0.2.19"
2
+ "backendCore": "^1.0.14",
3
+ "frontCore": "^0.2.20"
4
4
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xfilecom/xframe",
3
- "version": "0.1.33",
3
+ "version": "0.1.34",
4
4
  "description": "Scaffold full-stack app: Nest + @xfilecom/backend-core, Vite/React + @xfilecom/front-core",
5
5
  "license": "UNLICENSED",
6
6
  "bin": {
@@ -28,6 +28,32 @@ function SectionTitle({ children }: { children: ReactNode }) {
28
28
  );
29
29
  }
30
30
 
31
+ function StarIcon() {
32
+ return (
33
+ <svg width="16" height="16" viewBox="0 0 16 16" aria-hidden style={{ display: 'block' }}>
34
+ <path
35
+ fill="currentColor"
36
+ d="m8 1.3 1.9 4.2 4.6.4-3.5 3.1 1 4.5L8 11.5l-4 2 1-4.5-3.5-3.1 4.6-.4L8 1.3z"
37
+ />
38
+ </svg>
39
+ );
40
+ }
41
+
42
+ function CheckIcon() {
43
+ return (
44
+ <svg width="16" height="16" viewBox="0 0 16 16" aria-hidden style={{ display: 'block' }}>
45
+ <path
46
+ fill="none"
47
+ stroke="currentColor"
48
+ strokeWidth="2"
49
+ strokeLinecap="round"
50
+ strokeLinejoin="round"
51
+ d="M3.5 8.5l3 3 6-7"
52
+ />
53
+ </svg>
54
+ );
55
+ }
56
+
31
57
  export function FrontCoreShowcase() {
32
58
  const uid = useId();
33
59
  const toastSeq = useRef(0);
@@ -35,11 +61,16 @@ export function FrontCoreShowcase() {
35
61
  const [toasts, setToasts] = useState<ToastEntry[]>([]);
36
62
  const [errors, setErrors] = useState<InlineErrorEntry[]>([]);
37
63
  const [loading, setLoading] = useState(false);
64
+ const [badgeClicks, setBadgeClicks] = useState(0);
65
+ const [btnLoading, setBtnLoading] = useState(false);
38
66
  const [dialogOpen, setDialogOpen] = useState(false);
67
+ const [dialogRichOpen, setDialogRichOpen] = useState(false);
39
68
  const [confirmOpen, setConfirmOpen] = useState(false);
40
69
  const [confirmDestructiveOpen, setConfirmDestructiveOpen] = useState(false);
41
70
  const [confirmLoadingDemo, setConfirmLoadingDemo] = useState(false);
71
+ const [confirmCustomOpen, setConfirmCustomOpen] = useState(false);
42
72
  const [sheetOpen, setSheetOpen] = useState(false);
73
+ const [sheetRichOpen, setSheetRichOpen] = useState(false);
43
74
 
44
75
  const pushToast = useCallback(
45
76
  (severity: ToastSeverity) => {
@@ -50,17 +81,56 @@ export function FrontCoreShowcase() {
50
81
  [uid],
51
82
  );
52
83
 
84
+ const pushToastCustomIcon = useCallback(() => {
85
+ toastSeq.current += 1;
86
+ const id = `${uid}-t-${toastSeq.current}`;
87
+ setToasts((prev) => [
88
+ ...prev,
89
+ {
90
+ id,
91
+ severity: 'success',
92
+ message: 'ToastEntry.icon 으로 별 아이콘',
93
+ icon: <StarIcon />,
94
+ },
95
+ ]);
96
+ }, [uid]);
97
+
53
98
  const addError = useCallback(() => {
54
99
  errorSeq.current += 1;
55
100
  const id = `${uid}-e-${errorSeq.current}`;
56
101
  setErrors((prev) => [...prev, { id, message: `샘플 에러 #${errorSeq.current}` }]);
57
102
  }, [uid]);
58
103
 
104
+ const addErrorRich = useCallback(() => {
105
+ errorSeq.current += 1;
106
+ const id = `${uid}-e-${errorSeq.current}`;
107
+ setErrors((prev) => [
108
+ ...prev,
109
+ {
110
+ id,
111
+ message: '아이콘 + 행 클릭(콘솔)',
112
+ icon: <ToastSeverityIcon severity="error" />,
113
+ onClick: () => {
114
+ console.info('[FrontCoreShowcase] inline error row click', id);
115
+ },
116
+ },
117
+ ]);
118
+ }, [uid]);
119
+
59
120
  return (
60
121
  <>
61
- <InlineErrorList errors={errors} onDismiss={(id) => setErrors((e) => e.filter((x) => x.id !== id))} />
122
+ <InlineErrorList
123
+ errors={errors}
124
+ onDismiss={(id) => setErrors((e) => e.filter((x) => x.id !== id))}
125
+ dismissAriaLabel="배너 닫기"
126
+ />
62
127
  <ToastList toasts={toasts} onDismiss={(id) => setToasts((t) => t.filter((x) => x.id !== id))} />
63
- <LoadingOverlay active={loading} message="로딩 중…" />
128
+ <LoadingOverlay
129
+ active={loading}
130
+ title="처리 중"
131
+ message="데이터를 가져옵니다…"
132
+ ariaLabel="데이터 로딩 중"
133
+ />
64
134
 
65
135
  <Dialog
66
136
  open={dialogOpen}
@@ -79,6 +149,26 @@ export function FrontCoreShowcase() {
79
149
  </div>
80
150
  </Dialog>
81
151
 
152
+ <Dialog
153
+ open={dialogRichOpen}
154
+ onOpenChange={setDialogRichOpen}
155
+ title="헤더 커스텀"
156
+ titleAdornment={<ToastSeverityIcon severity="info" />}
157
+ headerExtra={
158
+ <Button variant="ghost" type="button" onClick={() => setDialogRichOpen(false)} aria-label="닫기">
159
+
160
+ </Button>
161
+ }
162
+ description="titleAdornment · headerExtra — Dialog props 로 조합"
163
+ >
164
+ <Text variant="body">bodyClassName · descriptionClassName 으로 패널 영역 클래스도 조정 가능합니다.</Text>
165
+ <div className="xfc-dialog-footer">
166
+ <Button type="button" variant="primary" onClick={() => setDialogRichOpen(false)}>
167
+ OK
168
+ </Button>
169
+ </div>
170
+ </Dialog>
171
+
82
172
  <ConfirmDialog
83
173
  open={confirmOpen}
84
174
  onOpenChange={setConfirmOpen}
@@ -106,6 +196,24 @@ export function FrontCoreShowcase() {
106
196
  onConfirm={() => {}}
107
197
  />
108
198
 
199
+ <ConfirmDialog
200
+ open={confirmCustomOpen}
201
+ onOpenChange={setConfirmCustomOpen}
202
+ title="버튼 props 확장"
203
+ message="confirmButtonProps / cancelButtonProps 로 icon·className 등 전달"
204
+ confirmLabel="저장"
205
+ cancelLabel="닫기"
206
+ onConfirm={() => setConfirmCustomOpen(false)}
207
+ confirmButtonProps={{
208
+ icon: <CheckIcon />,
209
+ iconPosition: 'start',
210
+ }}
211
+ cancelButtonProps={{
212
+ icon: <span aria-hidden>←</span>,
213
+ iconPosition: 'start',
214
+ }}
215
+ />
216
+
109
217
  <BottomSheet open={sheetOpen} onOpenChange={setSheetOpen} title="BottomSheet" showHandle>
110
218
  <Text variant="body">화면 하단에서 올라오는 패널입니다.</Text>
111
219
  <Stack direction="row" gap="sm" align="center" style={{ marginTop: 'var(--xfc-space-md)', flexWrap: 'wrap' }}>
@@ -115,6 +223,21 @@ export function FrontCoreShowcase() {
115
223
  </Stack>
116
224
  </BottomSheet>
117
225
 
226
+ <BottomSheet
227
+ open={sheetRichOpen}
228
+ onOpenChange={setSheetRichOpen}
229
+ title="액션 시트"
230
+ subtitle="subtitle · headerExtra 로 상단 구성"
231
+ headerExtra={
232
+ <Button variant="ghost" type="button" onClick={() => setSheetRichOpen(false)} aria-label="닫기">
233
+ 완료
234
+ </Button>
235
+ }
236
+ showHandle
237
+ >
238
+ <Text variant="body">BottomSheet bodyClassName 등으로 본문 영역도 조정할 수 있습니다.</Text>
239
+ </BottomSheet>
240
+
118
241
  <Stack direction="column" gap="lg" align="stretch">
119
242
  <Card>
120
243
  <Stack direction="column" gap="md" align="stretch">
@@ -125,24 +248,41 @@ export function FrontCoreShowcase() {
125
248
  <Text variant="muted">muted</Text>
126
249
  <Text variant="small">small</Text>
127
250
  <Text variant="accent">accent</Text>
251
+ <Text variant="small" style={{ color: 'var(--xfc-fg-muted)' }}>
252
+ truncate (좁은 영역)
253
+ </Text>
254
+ <div style={{ maxWidth: 200 }}>
255
+ <Text variant="body" truncate title="말줄임 전체 텍스트은 title 속성으로 툴팁">
256
+ 아주 긴 한 줄 텍스트는 truncate prop 으로 말줄임 처리합니다.
257
+ </Text>
258
+ </div>
128
259
  </Stack>
129
260
  </Card>
130
261
 
131
262
  <Card>
132
263
  <Stack direction="column" gap="md" align="stretch">
133
- <SectionTitle>Badge</SectionTitle>
134
- <Stack direction="row" gap="sm" align="center">
264
+ <SectionTitle>Badge · icon · enabled · onClick</SectionTitle>
265
+ <Stack direction="row" gap="sm" align="center" style={{ flexWrap: 'wrap' }}>
135
266
  <Badge tone="neutral">neutral</Badge>
136
267
  <Badge tone="accent">accent</Badge>
137
268
  <Badge tone="success">success</Badge>
138
269
  <Badge tone="danger">danger</Badge>
270
+ <Badge tone="accent" icon={<StarIcon />}>
271
+ icon
272
+ </Badge>
273
+ <Badge tone="neutral" enabled={false}>
274
+ enabled=false
275
+ </Badge>
276
+ <Badge tone="accent" icon={<CheckIcon />} onClick={() => setBadgeClicks((c) => c + 1)}>
277
+ 클릭 {badgeClicks}
278
+ </Badge>
139
279
  </Stack>
140
280
  </Stack>
141
281
  </Card>
142
282
 
143
283
  <Card>
144
284
  <Stack direction="column" gap="md" align="stretch">
145
- <SectionTitle>Button</SectionTitle>
285
+ <SectionTitle>Button · enabled · loading · icon</SectionTitle>
146
286
  <Stack direction="row" gap="sm" align="center" style={{ flexWrap: 'wrap' }}>
147
287
  <Button variant="primary">primary</Button>
148
288
  <Button variant="secondary">secondary</Button>
@@ -152,26 +292,85 @@ export function FrontCoreShowcase() {
152
292
  <Button variant="primary" disabled>
153
293
  disabled
154
294
  </Button>
295
+ <Button variant="primary" enabled={false}>
296
+ enabled=false
297
+ </Button>
298
+ <Button variant="primary" icon={<StarIcon />} iconPosition="start">
299
+ 아이콘 앞
300
+ </Button>
301
+ <Button variant="outline" icon={<CheckIcon />} iconPosition="end">
302
+ 아이콘 뒤
303
+ </Button>
304
+ <Button
305
+ variant="secondary"
306
+ loading={btnLoading}
307
+ onClick={() => {
308
+ setBtnLoading(true);
309
+ window.setTimeout(() => setBtnLoading(false), 1200);
310
+ }}
311
+ >
312
+ loading 데모
313
+ </Button>
155
314
  </Stack>
156
315
  </Stack>
157
316
  </Card>
158
317
 
318
+ <Card
319
+ title="Card · title / footer"
320
+ footer={
321
+ <Stack direction="row" justify="end" gap="sm" align="center">
322
+ <Button variant="ghost" type="button">
323
+ 보조
324
+ </Button>
325
+ <Button variant="primary" type="button">
326
+ 주 액션
327
+ </Button>
328
+ </Stack>
329
+ }
330
+ >
331
+ <Text variant="body">
332
+ Card 의 title·footer 가 있으면 본문이 .xfc-card__body 로 감싸져 패딩이 맞춰집니다. 루트에 onClick 등
333
+ HTML 속성을 그대로 넘길 수 있습니다.
334
+ </Text>
335
+ </Card>
336
+
159
337
  <Card>
160
338
  <Stack direction="column" gap="md" align="stretch">
161
- <SectionTitle>Input</SectionTitle>
339
+ <SectionTitle>Input · description · invalid · enabled</SectionTitle>
162
340
  <label htmlFor={`${uid}-email`}>
163
341
  <Text as="span" variant="labelBlock">
164
342
  이메일
165
343
  </Text>
166
344
  </label>
167
345
  <Input id={`${uid}-email`} type="email" placeholder="you@example.com" autoComplete="email" />
168
- <Input placeholder="비활성" disabled />
346
+ <Input
347
+ placeholder="형식 오류 예시"
348
+ invalid
349
+ description="invalid + description (aria-describedby 자동)"
350
+ defaultValue="not-an-email"
351
+ />
352
+ <Input placeholder="enabled=false" enabled={false} />
353
+ <Input placeholder="비활성(disabled)" disabled />
169
354
  </Stack>
170
355
  </Card>
171
356
 
172
357
  <Card>
173
358
  <Stack direction="column" gap="md" align="stretch">
174
- <SectionTitle>Box · Stack · Card</SectionTitle>
359
+ <SectionTitle>Box · Stack · 레이아웃 props</SectionTitle>
360
+ <Box
361
+ as="section"
362
+ padding="md"
363
+ style={{ border: '1px dashed var(--xfc-border-strong)', borderRadius: 'var(--xfc-radius-xs)' }}
364
+ aria-label="섹션 박스 예시"
365
+ >
366
+ <Text variant="small">Box as=&quot;section&quot; + padding</Text>
367
+ </Box>
368
+ <Stack direction="row" justify="between" align="center" wrap={false} gap="md">
369
+ <Text variant="small">justify=&quot;between&quot;</Text>
370
+ <Button variant="ghost" type="button">
371
+ 오른쪽
372
+ </Button>
373
+ </Stack>
175
374
  <Stack direction="row" gap="md" align="stretch" style={{ flexWrap: 'wrap' }}>
176
375
  <Box padding="md" style={{ border: '1px dashed var(--xfc-border-strong)', borderRadius: 'var(--xfc-radius-xs)' }}>
177
376
  <Text variant="small">Box padding md</Text>
@@ -185,20 +384,29 @@ export function FrontCoreShowcase() {
185
384
 
186
385
  <Card>
187
386
  <Stack direction="column" gap="md" align="stretch">
188
- <SectionTitle>ToastSeverityIcon</SectionTitle>
387
+ <SectionTitle>ToastSeverityIcon · className / style</SectionTitle>
189
388
  <Stack direction="row" gap="lg" align="center" style={{ color: 'var(--xfc-fg)' }}>
190
389
  <ToastSeverityIcon severity="info" />
191
390
  <ToastSeverityIcon severity="success" />
192
391
  <ToastSeverityIcon severity="warn" />
193
392
  <ToastSeverityIcon severity="error" />
393
+ <ToastSeverityIcon severity="info" style={{ opacity: 0.45 }} />
194
394
  </Stack>
195
395
  </Stack>
196
396
  </Card>
197
397
 
198
398
  <Card>
199
399
  <Stack direction="column" gap="md" align="stretch">
200
- <SectionTitle>Toast (단일)</SectionTitle>
201
- <Toast severity="info" message="인라인 Toast 한 줄" onDismiss={() => {}} />
400
+ <SectionTitle>Toast (단일) · icon · dismissible</SectionTitle>
401
+ <Toast severity="info" message="기본 닫기 버튼" onDismiss={() => {}} />
402
+ <Toast severity="warn" message="아이콘 숨김 (icon=null)" icon={null} onDismiss={() => {}} />
403
+ <Toast
404
+ severity="success"
405
+ message="커스텀 dismiss 라벨"
406
+ dismissAriaLabel="토스트 제거"
407
+ onDismiss={() => {}}
408
+ />
409
+ <Toast severity="error" message="닫기 없음" dismissible={false} />
202
410
  </Stack>
203
411
  </Card>
204
412
 
@@ -206,12 +414,14 @@ export function FrontCoreShowcase() {
206
414
  <Stack direction="column" gap="md" align="stretch">
207
415
  <SectionTitle>Dialog · ConfirmDialog · BottomSheet</SectionTitle>
208
416
  <Text variant="small" style={{ color: 'var(--xfc-fg-muted)' }}>
209
- document.body 포털 · z-index 10050 (토스트·로딩보다 위). base.css 의 .xfc-dialog-* /
210
- .xfc-bottom-sheet-* 로 테마 조정.
417
+ document.body 포털 · z-index 10050. base.css 의 .xfc-dialog-* / .xfc-bottom-sheet-* 로 테마 조정.
211
418
  </Text>
212
419
  <Stack direction="row" gap="sm" align="center" style={{ flexWrap: 'wrap' }}>
213
420
  <Button type="button" variant="secondary" onClick={() => setDialogOpen(true)}>
214
- Dialog 열기
421
+ Dialog
422
+ </Button>
423
+ <Button type="button" variant="secondary" onClick={() => setDialogRichOpen(true)}>
424
+ Dialog (헤더 커스텀)
215
425
  </Button>
216
426
  <Button type="button" variant="secondary" onClick={() => setConfirmOpen(true)}>
217
427
  Confirm
@@ -222,9 +432,15 @@ export function FrontCoreShowcase() {
222
432
  <Button type="button" variant="secondary" onClick={() => setConfirmLoadingDemo(true)}>
223
433
  Confirm (loading)
224
434
  </Button>
435
+ <Button type="button" variant="secondary" onClick={() => setConfirmCustomOpen(true)}>
436
+ Confirm (버튼 props)
437
+ </Button>
225
438
  <Button type="button" variant="secondary" onClick={() => setSheetOpen(true)}>
226
439
  BottomSheet
227
440
  </Button>
441
+ <Button type="button" variant="secondary" onClick={() => setSheetRichOpen(true)}>
442
+ BottomSheet (subtitle·extra)
443
+ </Button>
228
444
  </Stack>
229
445
  </Stack>
230
446
  </Card>
@@ -233,7 +449,7 @@ export function FrontCoreShowcase() {
233
449
  <Stack direction="column" gap="md" align="stretch">
234
450
  <SectionTitle>ToastList · InlineErrorList · LoadingOverlay</SectionTitle>
235
451
  <Text variant="small" style={{ color: 'var(--xfc-fg-muted)' }}>
236
- ToastList / InlineErrorList / LoadingOverlay 는 화면 고정 레이어입니다. 아래 버튼으로 띄워 보세요.
452
+ ToastList / InlineErrorList / LoadingOverlay 는 화면 고정 레이어입니다.
237
453
  </Text>
238
454
  <Stack direction="row" gap="sm" align="center" style={{ flexWrap: 'wrap' }}>
239
455
  <Button type="button" variant="secondary" onClick={() => pushToast('info')}>
@@ -248,15 +464,24 @@ export function FrontCoreShowcase() {
248
464
  <Button type="button" variant="secondary" onClick={() => pushToast('error')}>
249
465
  toast error
250
466
  </Button>
467
+ <Button type="button" variant="secondary" onClick={pushToastCustomIcon}>
468
+ toast 커스텀 icon
469
+ </Button>
251
470
  </Stack>
252
471
  <Stack direction="row" gap="sm" align="center" style={{ flexWrap: 'wrap' }}>
253
472
  <Button type="button" variant="outline" onClick={addError}>
254
473
  에러 배너 추가
255
474
  </Button>
475
+ <Button type="button" variant="outline" onClick={addErrorRich}>
476
+ 에러 (icon·onClick)
477
+ </Button>
256
478
  <Button type="button" variant="outline" onClick={() => setLoading((v) => !v)}>
257
479
  로딩 오버레이 {loading ? '끄기' : '켜기'}
258
480
  </Button>
259
481
  </Stack>
482
+ <Text variant="label">
483
+ LoadingOverlay: title · message · ariaLabel · spinner(null 로 숨김) · enabled
484
+ </Text>
260
485
  </Stack>
261
486
  </Card>
262
487
  </Stack>