@ihabdevteam/core 0.38.0 → 0.39.0
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/dist/components.cjs.js +15 -15
- package/dist/hooks.cjs.js +1 -788
- package/dist/ihabdevteam-core.cjs.js +32 -32
- package/dist/ihabdevteam-core.d.ts +45 -5
- package/dist/packages/core/src/components/BottomNav.js +1 -1
- package/dist/packages/core/src/components/DatePicker.js +1 -1
- package/dist/packages/core/src/components/Dropdown.js +1 -1
- package/dist/packages/core/src/components/FilterPanel.js +2 -1
- package/dist/packages/core/src/components/Header.js +1 -1
- package/dist/packages/core/src/components/Panel.d.ts +3 -133
- package/dist/packages/core/src/components/Panel.js +2 -140
- package/dist/packages/core/src/components/PanelRow.d.ts +145 -0
- package/dist/packages/core/src/components/PanelRow.js +144 -0
- package/dist/packages/core/src/components/Training/AudioPlayerWaveForm.js +1 -54
- package/dist/packages/core/src/components/Training/NoisePlayerContext.d.ts +2 -15
- package/dist/packages/core/src/components/useAnchoredMenu.d.ts +8 -26
- package/dist/packages/core/src/components/useAnchoredMenu.js +7 -70
- package/dist/packages/core/src/components/useSoundCompassExam.d.ts +8 -51
- package/dist/packages/core/src/components/useSoundCompassExam.js +10 -128
- package/dist/packages/core/src/components.d.ts +4 -4
- package/dist/packages/core/src/components.js +2 -2
- package/dist/packages/core/src/hooks/useAnchoredMenu.d.ts +28 -0
- package/dist/packages/core/src/hooks/useAnchoredMenu.js +72 -0
- package/dist/packages/core/src/hooks/useSoundCompassExam.d.ts +54 -0
- package/dist/packages/core/src/hooks/useSoundCompassExam.js +128 -0
- package/dist/packages/core/src/hooks/useWordAttributeFilter.d.ts +2 -0
- package/dist/packages/core/src/hooks/useWordAttributeFilter.js +2 -1
- package/dist/packages/core/src/hooks/useWordTrainingDataGeneration.d.ts +2 -8
- package/dist/packages/core/src/hooks/useWordTrainingFilter.js +7 -6
- package/dist/packages/core/src/hooks.d.ts +4 -0
- package/dist/packages/core/src/hooks.js +6 -0
- package/dist/packages/core/src/types/filterPanel.d.ts +15 -0
- package/dist/packages/core/src/types/training.d.ts +20 -0
- package/dist/packages/core/src/types/training.js +1 -0
- package/dist/packages/core/src/utils/audioPeaks.d.ts +2 -0
- package/dist/packages/core/src/utils/audioPeaks.js +67 -0
- package/dist/packages/core/src/utils/noiseSettingsStore.d.ts +1 -1
- package/dist/packages/core/src/utils/wordTrainingGroups.d.ts +1 -1
- package/dist/packages/core/src/utils/wordTrainingRuntimeTypes.d.ts +14 -0
- package/dist/packages/core/src/version.d.ts +1 -1
- package/dist/packages/core/src/version.js +1 -1
- package/dist/utils.cjs.js +1 -1
- package/package.json +1 -1
|
@@ -389,6 +389,17 @@ export declare interface BarData {
|
|
|
389
389
|
parts?: number[];
|
|
390
390
|
}
|
|
391
391
|
|
|
392
|
+
/**
|
|
393
|
+
* 패널 **한 줄** — `Panel` 이 담는 그릇이라면 이쪽은 그 안에 서는 줄이다.
|
|
394
|
+
*
|
|
395
|
+
* `Panel.tsx` 에서 갈라 나왔다. 한 파일에 컴포넌트 넷(Panel · PanelGrid · PanelRow ·
|
|
396
|
+
* PanelRowDevice)과 타입 열다섯이 함께 있었는데, 그릇을 고치러 들어온 사람이 줄의
|
|
397
|
+
* 12갈래 판별 합집합을 지나야 했다. 이 저장소가 이미 쓰는 방식이다
|
|
398
|
+
* (`trainingStepAudioPlayerTypes.ts` · `audioPlaybackRules.ts`).
|
|
399
|
+
*
|
|
400
|
+
* `Panel.tsx` 가 이것들을 다시 내보내므로 `@ihabdevteam/core/components/Panel` 에서
|
|
401
|
+
* 가져가던 곳은 그대로 돌아간다.
|
|
402
|
+
*/
|
|
392
403
|
declare type Base = {
|
|
393
404
|
label?: string;
|
|
394
405
|
sub?: string;
|
|
@@ -527,6 +538,7 @@ export declare function buildDropFinalConsonantProblem(word: string, _comp: Comp
|
|
|
527
538
|
export declare function buildGradeSection(t: TFunction, keys: WordAttributeFilterKeys, opts?: {
|
|
528
539
|
allLabel?: string;
|
|
529
540
|
labelAction?: ReactNode;
|
|
541
|
+
labelInfo?: FilterPanelSection['labelInfo'];
|
|
530
542
|
}): FilterPanelSection;
|
|
531
543
|
|
|
532
544
|
/**
|
|
@@ -597,6 +609,7 @@ export declare function buildRemoveConsonantProblem(word: string, _comp: Composi
|
|
|
597
609
|
export declare function buildSelectSection(t: TFunction, opts?: {
|
|
598
610
|
includeStructure?: boolean;
|
|
599
611
|
labelAction?: ReactNode;
|
|
612
|
+
labelInfo?: FilterPanelSection['labelInfo'];
|
|
600
613
|
}): FilterPanelSection;
|
|
601
614
|
|
|
602
615
|
/** 위치 p를 와일드카드('*')로 마스킹한 "나머지 음절이 같은" 2·3음절 실단어 그룹(O(1) 조회). */
|
|
@@ -1282,14 +1295,19 @@ export declare function copyText(text: string): Promise<boolean>;
|
|
|
1282
1295
|
*
|
|
1283
1296
|
* 값은 손으로 적고 `test/coreVersion.test.ts` 가 package.json 과 어긋나면 운다.
|
|
1284
1297
|
*/
|
|
1285
|
-
export declare const CORE_VERSION = "0.
|
|
1298
|
+
export declare const CORE_VERSION = "0.39.0";
|
|
1286
1299
|
|
|
1287
1300
|
/** @public */
|
|
1288
1301
|
declare const coreDefault: Record<string, unknown>;
|
|
1289
1302
|
export default coreDefault;
|
|
1290
1303
|
|
|
1291
|
-
/**
|
|
1292
|
-
|
|
1304
|
+
/**
|
|
1305
|
+
* 훈련 화면의 소음/재생 설정. `NoisePlayerContext` 가 들고 다니고
|
|
1306
|
+
* `utils/noiseSettingsStore` 가 앱이 주입한 콜백으로 영속화한다.
|
|
1307
|
+
* 두 계층이 함께 아는 모양이라 여기(아래 계층)에 둔다.
|
|
1308
|
+
* @public
|
|
1309
|
+
*/
|
|
1310
|
+
export declare type CoreNoiseSettings = {
|
|
1293
1311
|
enabled: boolean;
|
|
1294
1312
|
type: CoreNoiseType;
|
|
1295
1313
|
/** 0–9. 0이면 소음 비활성화. (소음 음원을 +7dB 증폭한 뒤 상한을 4→9로 확장 — 실제 volume = level/9) */
|
|
@@ -1302,8 +1320,8 @@ declare type CoreNoiseSettings = {
|
|
|
1302
1320
|
mainSpeed: number;
|
|
1303
1321
|
};
|
|
1304
1322
|
|
|
1305
|
-
/** @public */
|
|
1306
|
-
declare type CoreNoiseType = 'defaultNoise' | 'street' | 'restaurant' | 'buzz';
|
|
1323
|
+
/** 소음 음원 갈래. @public */
|
|
1324
|
+
export declare type CoreNoiseType = 'defaultNoise' | 'street' | 'restaurant' | 'buzz';
|
|
1307
1325
|
|
|
1308
1326
|
/** @public */
|
|
1309
1327
|
export declare function CoreScope({ as: Component, className, children, storage, ...rest }: CoreScopeProps): JSX.Element;
|
|
@@ -1933,6 +1951,21 @@ export declare interface FilterPanelSection {
|
|
|
1933
1951
|
/** 가로 배치에서 이 섹션을 한 줄 전체(자체 줄)로 배치 — 인라인 흐름에서 분리 */
|
|
1934
1952
|
block?: boolean;
|
|
1935
1953
|
labelAction?: ReactNode;
|
|
1954
|
+
/**
|
|
1955
|
+
* 라벨 옆 "?" 설명 버튼을 **서술로** 지정한다 — `labelAction` 에 요소를 직접 넣는 대신
|
|
1956
|
+
* 내용만 준다. FilterPanel 이 `FilterSectionInfo` 로 그린다.
|
|
1957
|
+
*
|
|
1958
|
+
* 왜 두 갈래인가: 훅은 화면을 그리면 안 된다. 섹션을 만들어 주는 훅
|
|
1959
|
+
* (`useWordTrainingFilter`)이 예전에는 `createElement(FilterSectionInfo, …)` 로
|
|
1960
|
+
* 요소를 만들었는데, 그러려면 **훅이 컴포넌트를 올려다봐야** 했다. 이 슬롯을 쓰면
|
|
1961
|
+
* 훅은 내용만 넘기고 그리는 일은 컴포넌트가 한다.
|
|
1962
|
+
* `labelAction` 은 그대로 둔다 — 호출부가 제 요소를 넣고 싶을 때가 있다.
|
|
1963
|
+
* 둘 다 있으면 `labelAction` 이 이긴다.
|
|
1964
|
+
*/
|
|
1965
|
+
labelInfo?: {
|
|
1966
|
+
title?: string;
|
|
1967
|
+
items: string[];
|
|
1968
|
+
};
|
|
1936
1969
|
items?: (FilterPanelItem | string)[];
|
|
1937
1970
|
groups?: (FilterPanelItem | string)[][];
|
|
1938
1971
|
/** 'note': 선택 불가능한 안내 텍스트 섹션 — content를 그대로 렌더링한다(예: 입력 구문 사용법). */
|
|
@@ -8685,6 +8718,13 @@ export declare interface WordTrainingFilterCaps {
|
|
|
8685
8718
|
/** @public 훈련 유형이 속한 상위 그룹(카테고리). 앱의 STATIC_TYPE_GROUPS와 동일 분류. */
|
|
8686
8719
|
declare type WordTrainingGroup = 'discrimination' | 'synthesis' | 'deletion' | 'substitution' | 'attention';
|
|
8687
8720
|
|
|
8721
|
+
/**
|
|
8722
|
+
* 훈련 갈래 한 줄 — 열쇠와 화면에 보일 이름표.
|
|
8723
|
+
* `hooks/useWordTrainingDataGeneration` 이 만들어 내보내고
|
|
8724
|
+
* `utils/wordTrainingGroups` 가 묶을 때 읽는다. 두 계층이 함께 아는 모양이라
|
|
8725
|
+
* 열쇠 타입이 있는 여기(아래 계층)에 둔다.
|
|
8726
|
+
* @public
|
|
8727
|
+
*/
|
|
8688
8728
|
export declare interface WordTrainingType {
|
|
8689
8729
|
key: WordTrainingTypeKey;
|
|
8690
8730
|
label: string;
|
|
@@ -9,7 +9,7 @@ import MenuTooltip from './Tooltip/MenuTooltip';
|
|
|
9
9
|
// Tooltip.tsx만 이 CSS를 import하는데 BottomNav는 그 컴포넌트를 거치지 않아,
|
|
10
10
|
// 프로덕션에서 청크가 갈리면 스타일 없이 렌더될 수 있다.
|
|
11
11
|
import './Tooltip/Tooltip.css';
|
|
12
|
-
import { useAnchoredMenu } from '
|
|
12
|
+
import { useAnchoredMenu } from '../hooks/useAnchoredMenu';
|
|
13
13
|
import { useTranslation } from 'react-i18next';
|
|
14
14
|
const BTN_SIZE = 52;
|
|
15
15
|
const BTN_GAP = 10;
|
|
@@ -13,7 +13,7 @@ import Calendar from 'lucide-react/dist/esm/icons/calendar.mjs';
|
|
|
13
13
|
import ChevronLeft from 'lucide-react/dist/esm/icons/chevron-left.mjs';
|
|
14
14
|
import ChevronRight from 'lucide-react/dist/esm/icons/chevron-right.mjs';
|
|
15
15
|
import Dropdown from './Dropdown';
|
|
16
|
-
import { useAnchoredMenu } from '
|
|
16
|
+
import { useAnchoredMenu } from '../hooks/useAnchoredMenu';
|
|
17
17
|
import { useCalendarMonth } from './useCalendarMonth';
|
|
18
18
|
import { FOCUSABLE_SELECTOR } from '../hooks/useFocusTrap';
|
|
19
19
|
import './DatePicker.css';
|
|
@@ -4,7 +4,7 @@ import { cx } from '../utils/cx';
|
|
|
4
4
|
import { useCallback, useEffect, useId, useRef, useState } from 'react';
|
|
5
5
|
import { createPortal } from 'react-dom';
|
|
6
6
|
import { useTranslation } from 'react-i18next';
|
|
7
|
-
import { useAnchoredMenu } from '
|
|
7
|
+
import { useAnchoredMenu } from '../hooks/useAnchoredMenu';
|
|
8
8
|
import './Dropdown.css';
|
|
9
9
|
/** @public */
|
|
10
10
|
export default function Dropdown({ options, value, onChange, placeholder, disabled = false, block = false, size = 48, error = false, className = '', ariaLabel }) {
|
|
@@ -10,6 +10,7 @@ import TextField from './TextField';
|
|
|
10
10
|
import Dropdown from './Dropdown';
|
|
11
11
|
export { FILTER_ALL_VALUE } from '../types/filterPanel';
|
|
12
12
|
import { FILTER_ALL_VALUE } from '../types/filterPanel';
|
|
13
|
+
import FilterSectionInfo from './FilterSectionInfo';
|
|
13
14
|
/** @public */
|
|
14
15
|
export default function FilterPanel({ title, headerAction, collapsible = false, defaultCollapsed = false, collapsed: controlledCollapsed, onCollapsedChange, footer, sections, selected = {}, onSelect, align, sectionAlign, className, }) {
|
|
15
16
|
// align 우선. 없으면 구 prop(sectionAlign) 매핑, 그것도 없으면 inline 기본.
|
|
@@ -49,7 +50,7 @@ export default function FilterPanel({ title, headerAction, collapsible = false,
|
|
|
49
50
|
const isSelected = Array.isArray(sel) ? sel.includes(value) : sel === value;
|
|
50
51
|
return (_jsx(Button, { size: "keyboard", label: itemLabels ?? displayLabel ?? value, type: section.itemType ?? (itemLabels ? 'bar' : value.length === 1 ? 'dot' : undefined), selected: isSelected, disabled: disabled, shape: shape, showTooltipInfo: !!tooltip, tooltipInfoText: tooltip, tooltipDescription: descriptionTooltip, onClick: () => onSelect?.(value, section.label) }, value));
|
|
51
52
|
};
|
|
52
|
-
return (_jsxs(Fragment, { children: [section.groupLabel && (_jsxs("div", { className: "filter-panel__group-label-row", children: [_jsx("div", { className: "filter-panel__group-label body-2", children: section.groupLabel }), section.groupAction] })), _jsxs("div", { className: cx('filter-panel__section', section.layout === 'row' ? 'filter-panel__section--row' : '', section.block ? 'filter-panel__section--block' : ''), children: [_jsxs("div", { className: "filter-panel__section-label-row", children: [_jsx("span", { className: "filter-panel__section-label body-3", children: section.displayLabel ?? section.label }), section.labelAction] }), section.type === 'note' ? (_jsx("div", { className: "filter-panel__note body-3", children: section.content })) : section.type === 'input' ? (_jsx(TextField, { className: "filter-panel__textfield", placeholder: section.placeholder, ariaLabel: section.displayLabel ?? section.label, value: selected[section.label] ?? '', onChange: (value) => onSelect?.(value, section.label), error: section.error })) : (!section.multiple && section.display === 'segmented') ? (_jsx(SegmentedControl
|
|
53
|
+
return (_jsxs(Fragment, { children: [section.groupLabel && (_jsxs("div", { className: "filter-panel__group-label-row", children: [_jsx("div", { className: "filter-panel__group-label body-2", children: section.groupLabel }), section.groupAction] })), _jsxs("div", { className: cx('filter-panel__section', section.layout === 'row' ? 'filter-panel__section--row' : '', section.block ? 'filter-panel__section--block' : ''), children: [_jsxs("div", { className: "filter-panel__section-label-row", children: [_jsx("span", { className: "filter-panel__section-label body-3", children: section.displayLabel ?? section.label }), section.labelAction ?? (section.labelInfo ? _jsx(FilterSectionInfo, { ...section.labelInfo }) : null)] }), section.type === 'note' ? (_jsx("div", { className: "filter-panel__note body-3", children: section.content })) : section.type === 'input' ? (_jsx(TextField, { className: "filter-panel__textfield", placeholder: section.placeholder, ariaLabel: section.displayLabel ?? section.label, value: selected[section.label] ?? '', onChange: (value) => onSelect?.(value, section.label), error: section.error })) : (!section.multiple && section.display === 'segmented') ? (_jsx(SegmentedControl
|
|
53
54
|
/* 바로 위 TextField 처럼 섹션 이름을 그대로 쓴다. 안 주면 라디오그룹이
|
|
54
55
|
기본값 '선택 탭'으로 읽혀, 필터가 여럿인 화면에서 어느 그룹인지
|
|
55
56
|
구분이 안 된다(단어표 검색 설정은 세그먼트가 열 개 가까이 된다). */
|
|
@@ -6,7 +6,7 @@ import { useMediaQuery } from '../hooks';
|
|
|
6
6
|
import BottomNav from './BottomNav';
|
|
7
7
|
import Button from './Button';
|
|
8
8
|
import MenuTooltip from './Tooltip/MenuTooltip';
|
|
9
|
-
import { useAnchoredMenu } from '
|
|
9
|
+
import { useAnchoredMenu } from '../hooks/useAnchoredMenu';
|
|
10
10
|
import { createPortal } from 'react-dom';
|
|
11
11
|
// 메뉴 표면(.tooltip-content) 정의 — Tooltip.tsx를 거치지 않고 클래스만 쓰므로 명시적으로.
|
|
12
12
|
import './Tooltip/Tooltip.css';
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import type { ReactNode
|
|
2
|
-
import type { DeviceEntry, DeviceSide } from './HearingDeviceInfo';
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
3
2
|
import './Panel.css';
|
|
4
3
|
type PanelSize = 'small' | 'medium' | 'large';
|
|
5
4
|
interface PanelProps {
|
|
@@ -51,134 +50,5 @@ export declare function PanelGrid({ cols, children }: {
|
|
|
51
50
|
cols?: number;
|
|
52
51
|
children: ReactNode;
|
|
53
52
|
}): import("react").JSX.Element;
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
sub?: string;
|
|
57
|
-
onClick?: () => void;
|
|
58
|
-
required?: boolean;
|
|
59
|
-
error?: string;
|
|
60
|
-
align?: 'horizontal' | 'vertical';
|
|
61
|
-
useFullWidth?: boolean;
|
|
62
|
-
icon?: ReactNode;
|
|
63
|
-
};
|
|
64
|
-
export type PanelRowToggle = Base & {
|
|
65
|
-
variant: 'toggle';
|
|
66
|
-
checked: boolean;
|
|
67
|
-
onChange: (v: boolean) => void;
|
|
68
|
-
};
|
|
69
|
-
export type PanelRowSegmented = Base & {
|
|
70
|
-
variant: 'segmented';
|
|
71
|
-
options: {
|
|
72
|
-
key: string;
|
|
73
|
-
label: string;
|
|
74
|
-
}[];
|
|
75
|
-
value: string;
|
|
76
|
-
onChange: (v: string) => void;
|
|
77
|
-
size?: 32 | 40 | 48;
|
|
78
|
-
};
|
|
79
|
-
export type DropdownItem = {
|
|
80
|
-
options: {
|
|
81
|
-
value: string;
|
|
82
|
-
label: string;
|
|
83
|
-
}[];
|
|
84
|
-
value: string;
|
|
85
|
-
onChange: (v: string) => void;
|
|
86
|
-
placeholder?: string;
|
|
87
|
-
/**
|
|
88
|
-
* 이 드롭다운 하나의 이름. 미지정 시 "행 라벨 + placeholder" 로 조합한다
|
|
89
|
-
* (생년월일 + 년 → "생년월일 년").
|
|
90
|
-
*/
|
|
91
|
-
ariaLabel?: string;
|
|
92
|
-
};
|
|
93
|
-
export type PanelRowDropdown = Base & {
|
|
94
|
-
variant: 'dropdown';
|
|
95
|
-
options?: {
|
|
96
|
-
value: string;
|
|
97
|
-
label: string;
|
|
98
|
-
}[];
|
|
99
|
-
value?: string;
|
|
100
|
-
onChange?: (v: string) => void;
|
|
101
|
-
dropdowns?: DropdownItem[];
|
|
102
|
-
};
|
|
103
|
-
export type ButtonItem = {
|
|
104
|
-
label: ReactNode;
|
|
105
|
-
btnVariant?: string;
|
|
106
|
-
btnSize?: number;
|
|
107
|
-
disabled?: boolean;
|
|
108
|
-
onClick: () => void;
|
|
109
|
-
};
|
|
110
|
-
export type PanelRowButton = Base & {
|
|
111
|
-
variant: 'button';
|
|
112
|
-
buttonLabel?: ReactNode;
|
|
113
|
-
btnVariant?: string;
|
|
114
|
-
btnSize?: number;
|
|
115
|
-
disabled?: boolean;
|
|
116
|
-
onClick?: () => void;
|
|
117
|
-
buttons?: ButtonItem[];
|
|
118
|
-
};
|
|
119
|
-
export type PanelRowText = Base & {
|
|
120
|
-
variant: 'text';
|
|
121
|
-
value: string;
|
|
122
|
-
valueColor?: string;
|
|
123
|
-
};
|
|
124
|
-
export type PanelRowInput = Base & {
|
|
125
|
-
variant: 'input';
|
|
126
|
-
value: string;
|
|
127
|
-
onChange?: (v: string) => void;
|
|
128
|
-
onKeyDown?: (e: KeyboardEvent<HTMLInputElement | HTMLTextAreaElement>) => void;
|
|
129
|
-
/** Enter로 확정(제출 등). TextField가 IME 조합 중 Enter는 걸러 주므로 한글 입력에서도 안전하다. */
|
|
130
|
-
onEnter?: () => void;
|
|
131
|
-
placeholder?: string;
|
|
132
|
-
inputMode?: 'none' | 'text' | 'decimal' | 'numeric' | 'tel' | 'search' | 'email' | 'url';
|
|
133
|
-
readOnly?: boolean;
|
|
134
|
-
disabled?: boolean;
|
|
135
|
-
type?: 'text' | 'email' | 'password' | 'number' | 'tel' | 'url';
|
|
136
|
-
autoComplete?: string;
|
|
137
|
-
helperText?: string;
|
|
138
|
-
/** 최대 글자 수 — 설정 시 입력 제한 + 박스 안쪽 글자수 카운터 표시 */
|
|
139
|
-
maxLength?: number;
|
|
140
|
-
/** 글자수 카운터 표시 여부 (maxLength 설정 시, 기본 true) */
|
|
141
|
-
showCount?: boolean;
|
|
142
|
-
/** 여러 줄 입력(textarea) 모드 */
|
|
143
|
-
multiline?: boolean;
|
|
144
|
-
/** multiline 모드 기본 행 수 */
|
|
145
|
-
rows?: number;
|
|
146
|
-
};
|
|
147
|
-
export type PanelRowCustom = Base & {
|
|
148
|
-
variant: 'custom';
|
|
149
|
-
children: ReactNode;
|
|
150
|
-
};
|
|
151
|
-
/** 라벨/우측 분리 없이 설명 문단을 통으로 표시하는 행 */
|
|
152
|
-
export type PanelRowDescription = {
|
|
153
|
-
variant: 'description';
|
|
154
|
-
children: ReactNode;
|
|
155
|
-
};
|
|
156
|
-
/** 빈 상태 표현 — Panel의 `empty` prop이 내부적으로 이 variant로 렌더한다. 단독으로도 사용 가능. */
|
|
157
|
-
export type PanelRowEmpty = {
|
|
158
|
-
variant: 'empty';
|
|
159
|
-
children: ReactNode;
|
|
160
|
-
};
|
|
161
|
-
/** 로딩 스피너 1개만 중앙에 표시 — Panel의 `loading` prop이 내부적으로 이 variant로 렌더한다. */
|
|
162
|
-
export type PanelRowLoading = {
|
|
163
|
-
variant: 'loading';
|
|
164
|
-
};
|
|
165
|
-
export type PanelRowChildren = {
|
|
166
|
-
label?: ReactNode;
|
|
167
|
-
sub?: string;
|
|
168
|
-
onClick?: () => void;
|
|
169
|
-
children?: ReactNode;
|
|
170
|
-
align?: 'horizontal' | 'vertical';
|
|
171
|
-
icon?: ReactNode;
|
|
172
|
-
};
|
|
173
|
-
export type PanelRowProps = PanelRowToggle | PanelRowSegmented | PanelRowDropdown | PanelRowButton | PanelRowText | PanelRowInput | PanelRowCustom | PanelRowDescription | PanelRowEmpty | PanelRowLoading | PanelRowChildren;
|
|
174
|
-
export declare function PanelRow(props: PanelRowProps): import("react").JSX.Element;
|
|
175
|
-
export type { DeviceSide };
|
|
176
|
-
export interface PanelRowDeviceProps {
|
|
177
|
-
devices: {
|
|
178
|
-
좌: DeviceEntry | null;
|
|
179
|
-
우: DeviceEntry | null;
|
|
180
|
-
};
|
|
181
|
-
/** 있으면 각 쪽 이름 옆에 '착용 해제'가 선다 — 그 쪽의 '지금 착용중' 만 내린다(이력은 남는다) */
|
|
182
|
-
onRelease?: (side: DeviceSide) => void;
|
|
183
|
-
}
|
|
184
|
-
export declare function PanelRowDevice({ devices, onRelease }: PanelRowDeviceProps): import("react").JSX.Element;
|
|
53
|
+
export { PanelRow, PanelRowDevice } from './PanelRow';
|
|
54
|
+
export type { PanelRowToggle, PanelRowSegmented, PanelRowDropdown, PanelRowButton, PanelRowText, PanelRowInput, PanelRowCustom, PanelRowDescription, PanelRowEmpty, PanelRowLoading, PanelRowChildren, PanelRowProps, PanelRowDeviceProps, DropdownItem, ButtonItem, DeviceSide, } from './PanelRow';
|
|
@@ -1,19 +1,9 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { cx } from '../utils/cx';
|
|
4
|
-
import {
|
|
5
|
-
import { useTranslation } from 'react-i18next';
|
|
6
|
-
import Toggle from './Toggle';
|
|
7
|
-
import SegmentedControl from './SegmentedControl';
|
|
8
|
-
import Dropdown from './Dropdown';
|
|
9
|
-
import Button from './Button';
|
|
10
|
-
import Badge from './Badge';
|
|
4
|
+
import { PanelRow } from './PanelRow';
|
|
11
5
|
import Icon from './Icon';
|
|
12
|
-
import { deviceKindVariant } from '../utils/deviceKind';
|
|
13
|
-
import TextField from './TextField';
|
|
14
|
-
import Loader from './Loader';
|
|
15
6
|
import './Panel.css';
|
|
16
|
-
const noop = () => { };
|
|
17
7
|
export default function Panel({ title, icon, disc, actions, size = 'medium', minHeight, maxWidth, className, scrollable, useBorder = true, useBg = true, useRowBorder = true, labelWidth, form = false, loading, empty, children }) {
|
|
18
8
|
const cls = cx('panel', `panel--${size}`, form ? 'panel--form' : '',
|
|
19
9
|
/* minHeight 패널은 마지막 행 아래 빈 공간이 생길 수 있으므로 행 하단 구분선 사용 */
|
|
@@ -32,132 +22,4 @@ export default function Panel({ title, icon, disc, actions, size = 'medium', min
|
|
|
32
22
|
export function PanelGrid({ cols = 2, children }) {
|
|
33
23
|
return (_jsx("div", { className: "panel-grid", style: { '--panel-grid-cols': cols }, children: children }));
|
|
34
24
|
}
|
|
35
|
-
export
|
|
36
|
-
// toggle 행의 라벨-스위치 연결용 id — 모든 variant에서 훅 호출 순서를 유지하기 위해
|
|
37
|
-
// 조건부 return들보다 앞에서 무조건 호출한다 (사용하지 않는 variant에서는 그냥 버려짐)
|
|
38
|
-
const uid = useId();
|
|
39
|
-
// children 기반 하위 호환 모드
|
|
40
|
-
if (!('variant' in props)) {
|
|
41
|
-
const { label, sub, onClick, children, align = 'horizontal', icon } = props;
|
|
42
|
-
const handleKeyDown = (e) => {
|
|
43
|
-
if (!onClick)
|
|
44
|
-
return;
|
|
45
|
-
// 우측 children 안의 버튼/토글 등이 자체적으로 Enter/Space를 처리하므로,
|
|
46
|
-
// 행 자기 자신에서 발생한 키 입력만 행 클릭으로 취급한다
|
|
47
|
-
if (e.target !== e.currentTarget)
|
|
48
|
-
return;
|
|
49
|
-
if (e.key === 'Enter' || e.key === ' ') {
|
|
50
|
-
e.preventDefault();
|
|
51
|
-
onClick();
|
|
52
|
-
}
|
|
53
|
-
};
|
|
54
|
-
return (_jsxs("div", { className: cx('panel-row', align === 'vertical' ? 'panel-row--vertical' : '', onClick ? 'panel-row--clickable' : ''), onClick: onClick, onKeyDown: onClick ? handleKeyDown : undefined, role: onClick ? 'button' : undefined, tabIndex: onClick ? 0 : undefined, children: [icon != null && _jsx("div", { className: "panel-row__icon", children: icon }), label != null && (_jsxs("div", { className: "panel-row__left", children: [_jsx("div", { className: "panel-row__label body-3", children: label }), sub && _jsx("div", { className: "panel-row__sub caption-1", children: sub })] })), _jsx("div", { className: "panel-row__right", children: children })] }));
|
|
55
|
-
}
|
|
56
|
-
// 설명 전용 행 — 라벨/우측 구조 없이 문단만 렌더
|
|
57
|
-
if (props.variant === 'description') {
|
|
58
|
-
const p = props;
|
|
59
|
-
return (_jsx("div", { className: "panel-row panel-row--block", children: _jsx("p", { className: "panel-row__description body-3", children: p.children }) }));
|
|
60
|
-
}
|
|
61
|
-
// 빈 상태 — 라벨 없이 중앙 정렬된 한 줄
|
|
62
|
-
if (props.variant === 'empty') {
|
|
63
|
-
const p = props;
|
|
64
|
-
return _jsx("div", { className: "panel-row panel-row--empty body-3", children: p.children });
|
|
65
|
-
}
|
|
66
|
-
// 로딩 — 중앙 스피너 1개
|
|
67
|
-
if (props.variant === 'loading') {
|
|
68
|
-
return _jsx("div", { className: "panel-row panel-row--loading", children: _jsx(Loader, { size: 28 }) });
|
|
69
|
-
}
|
|
70
|
-
// custom 행에 label이 없으면 좌/우 분할 없이 children을 그대로 꽉 채워 렌더
|
|
71
|
-
// (CodeBox처럼 라벨과 짝지을 수 없는 큰 블록 콘텐츠를 행 구조 안에 넣을 때 사용)
|
|
72
|
-
if (props.variant === 'custom' && !props.label) {
|
|
73
|
-
const p = props;
|
|
74
|
-
return (_jsx("div", { className: "panel-row panel-row--block", children: p.children }));
|
|
75
|
-
}
|
|
76
|
-
const { variant, label, sub, onClick, required, error, align = 'horizontal', useFullWidth = false, icon } = props;
|
|
77
|
-
let right = null;
|
|
78
|
-
// toggle 행만 왼쪽 라벨(panel-row__label)을 <label htmlFor>로 렌더해 스위치와 프로그래밍적으로
|
|
79
|
-
// 연결한다 — 그 전엔 시각적으로만 옆에 붙어있어 라벨 텍스트를 클릭해도 토글이 안 되고
|
|
80
|
-
// 스크린리더도 이 스위치가 뭘 위한 건지 안내하지 못했다.
|
|
81
|
-
const toggleId = variant === 'toggle' ? `panel-toggle-${uid}` : undefined;
|
|
82
|
-
if (variant === 'toggle') {
|
|
83
|
-
const p = props;
|
|
84
|
-
right = _jsx(Toggle, { checked: p.checked, onChange: p.onChange, id: toggleId });
|
|
85
|
-
}
|
|
86
|
-
else if (variant === 'segmented') {
|
|
87
|
-
const p = props;
|
|
88
|
-
// dropdown 행과 동일하게, PanelRow 왼쪽 라벨은 <label htmlFor>가 아닌 일반 div라서
|
|
89
|
-
// SegmentedControl에 ariaLabel로 넘겨야 스크린리더가 이름 없는 라디오그룹으로 안내하지 않는다.
|
|
90
|
-
right = _jsx(SegmentedControl, { options: p.options, value: p.value, onChange: p.onChange, size: p.size ?? 40, ariaLabel: label });
|
|
91
|
-
}
|
|
92
|
-
else if (variant === 'dropdown') {
|
|
93
|
-
const p = props;
|
|
94
|
-
const dropdowns = p.dropdowns?.length ? (_jsx("div", { className: `panel-row__dropdown-group${useFullWidth ? ' panel-row__dropdown-group--full' : ''}`, children: p.dropdowns.map((d, i) => (_jsx("div", { className: useFullWidth ? 'panel-row__dropdown-item--full' : undefined, children: _jsx(Dropdown, { options: d.options, value: d.value, onChange: d.onChange, placeholder: d.placeholder, block: useFullWidth, error: !!error,
|
|
95
|
-
// 한 행에 여러 개가 놓이므로 행 라벨만 넘기면 셋 다 "생년월일" 이라는 같은 이름이 된다.
|
|
96
|
-
// aria-label 이 있으면 스크린리더는 트리거의 화면 텍스트(년/월/일)를 읽지 않으므로
|
|
97
|
-
// 사용자는 어느 것이 무엇인지 구별할 수 없다 — placeholder 를 붙여 구별되게 한다.
|
|
98
|
-
ariaLabel: d.ariaLabel ?? ([label, d.placeholder].filter(Boolean).join(' ') || undefined) }) }, i))) })) : (
|
|
99
|
-
// PanelRow 왼쪽 라벨(panel-row__label)은 <label htmlFor>가 아니라 그냥 div라서 트리거 버튼과
|
|
100
|
-
// 프로그래밍적으로 연결되지 않는다 — ariaLabel로 직접 이름을 넘긴다 (TextField의 ariaLabel과 동일 패턴)
|
|
101
|
-
_jsx(Dropdown, { options: p.options ?? [], value: p.value ?? '', onChange: p.onChange ?? noop, block: useFullWidth, error: !!error, ariaLabel: label }));
|
|
102
|
-
right = error ? (_jsxs("div", { className: `panel-row__dropdown-error-wrap${useFullWidth ? ' panel-row__dropdown-error-wrap--full' : ''}`, children: [dropdowns, _jsx("span", { className: "caption-1 panel-row__error", role: "alert", children: error })] })) : dropdowns;
|
|
103
|
-
}
|
|
104
|
-
else if (variant === 'button') {
|
|
105
|
-
const p = props;
|
|
106
|
-
if (p.buttons?.length) {
|
|
107
|
-
right = (_jsx("div", { className: "panel-row__button-group", children: p.buttons.map((btn, i) => (_jsx(Button, { size: btn.btnSize ?? 40, variant: btn.btnVariant ?? 'default', disabled: btn.disabled, onClick: btn.onClick, children: btn.label }, i))) }));
|
|
108
|
-
}
|
|
109
|
-
else {
|
|
110
|
-
right = (_jsx(Button, { size: p.btnSize ?? 40, variant: p.btnVariant ?? 'default', disabled: p.disabled, onClick: p.onClick, children: p.buttonLabel }));
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
else if (variant === 'text') {
|
|
114
|
-
const p = props;
|
|
115
|
-
right = _jsx("span", { className: `panel-row__text body-2${useFullWidth ? ' panel-row__text--full' : ''}`, style: p.valueColor ? { color: p.valueColor } : undefined, children: p.value });
|
|
116
|
-
}
|
|
117
|
-
else if (variant === 'input') {
|
|
118
|
-
const p = props;
|
|
119
|
-
right = (_jsxs("div", { className: `panel-row__input-wrap${useFullWidth ? ' panel-row__input-wrap--full' : ''}`, children: [_jsx(TextField, { value: p.value, onChange: p.onChange ?? noop, onKeyDown: p.onKeyDown, onEnter: p.onEnter, placeholder: p.placeholder, inputMode: p.inputMode, readOnly: p.readOnly, disabled: p.disabled, type: p.type, autoComplete: p.autoComplete, maxLength: p.maxLength, showCount: p.showCount, multiline: p.multiline, rows: p.rows, fullWidth: useFullWidth, error: error, ariaLabel: label, required: required }), p.helperText && _jsx("div", { className: "panel-row__helper caption-1", children: p.helperText })] }));
|
|
120
|
-
}
|
|
121
|
-
else if (variant === 'custom') {
|
|
122
|
-
const p = props;
|
|
123
|
-
right = p.children;
|
|
124
|
-
}
|
|
125
|
-
const isLabelFixed = variant === 'input' || variant === 'dropdown' || ((variant === 'text' || variant === 'custom') && useFullWidth);
|
|
126
|
-
// 위 children 모드와 동일한 키보드 처리 — 이쪽 경로에만 빠져 있어서,
|
|
127
|
-
// variant 모드에 onClick을 주면 조용히 마우스 전용이 된다(현재 사용처는 없지만 예방).
|
|
128
|
-
const handleKeyDown = (e) => {
|
|
129
|
-
if (!onClick)
|
|
130
|
-
return;
|
|
131
|
-
if (e.target !== e.currentTarget)
|
|
132
|
-
return;
|
|
133
|
-
if (e.key === 'Enter' || e.key === ' ') {
|
|
134
|
-
e.preventDefault();
|
|
135
|
-
onClick();
|
|
136
|
-
}
|
|
137
|
-
};
|
|
138
|
-
return (_jsxs("div", { className: cx('panel-row', align === 'vertical' ? 'panel-row--vertical' : '', isLabelFixed ? 'panel-row--label-fixed' : '', variant === 'text' ? 'panel-row--text' : '', onClick ? 'panel-row--clickable' : ''), onClick: onClick, onKeyDown: onClick ? handleKeyDown : undefined, role: onClick ? 'button' : undefined, tabIndex: onClick ? 0 : undefined, children: [icon != null && _jsx("div", { className: "panel-row__icon", children: icon }), _jsxs("div", { className: "panel-row__left", children: [toggleId ? (_jsxs("label", { htmlFor: toggleId, className: "panel-row__label body-3", children: [label, required && _jsx("span", { className: "required-mark", children: "*" })] })) : (_jsxs("div", { className: "panel-row__label body-3", children: [label, required && _jsx("span", { className: "required-mark", children: "*" })] })), sub && _jsx("div", { className: "panel-row__sub caption-1", children: sub })] }), _jsx("div", { className: "panel-row__right", children: right })] }));
|
|
139
|
-
}
|
|
140
|
-
/**
|
|
141
|
-
* 제목 아래 한 줄로 세울 곁정보 — 브랜드·착용형태.
|
|
142
|
-
*
|
|
143
|
-
* 제목으로 이미 쓴 값과 빈 값, 그리고 서로 같은 값을 걷어 낸다. 걷어 내지 않으면
|
|
144
|
-
* 브랜드도 형태도 '기타' 인 기기가 **'기타 · 기타'** 로 떴다(같은 말이 두 번).
|
|
145
|
-
*/
|
|
146
|
-
function 메타(d) {
|
|
147
|
-
const 제목 = d.productName || d.brand;
|
|
148
|
-
const 조각 = [d.brand, d.fitType].filter((v) => !!v && v !== 제목);
|
|
149
|
-
return [...new Set(조각)].join(' · ');
|
|
150
|
-
}
|
|
151
|
-
export function PanelRowDevice({ devices, onRelease }) {
|
|
152
|
-
const { t } = useTranslation();
|
|
153
|
-
const SIDES = ['좌', '우'];
|
|
154
|
-
return (_jsx("div", { className: "panel-row panel-row--device", children: SIDES.map(side => {
|
|
155
|
-
const d = devices[side];
|
|
156
|
-
const sideLabel = side === '좌' ? t('device.direction.left', '좌') : t('device.direction.right', '우');
|
|
157
|
-
return (_jsxs("div", { className: "panel-row__device-column", children: [_jsxs("div", { className: "panel-row__device-head", children: [_jsx("span", { className: "panel-row__device-side body-2b", children: sideLabel }), d && onRelease && (
|
|
158
|
-
/* 표의 줄 끝 단추와 같은 모양·같은 말 — 하는 일도 같다.
|
|
159
|
-
이력에서 지우는 것이 아니라 '지금 끼고 있음' 만 내린다
|
|
160
|
-
(잠시 안 씀·수리 보냄 같은 경우가 실제로 있다). */
|
|
161
|
-
_jsx(Button, { size: 32, type: "dot", icon: "x", ariaLabel: t('customers.device.unwear', '착용 해제'), showTooltipInfo: t('customers.device.unwear', '착용 해제'), onClick: () => onRelease(side) }))] }), d ? (_jsxs("div", { className: "panel-row__device-info", children: [_jsxs("span", { className: "panel-row__device-kind", children: [d.device && _jsx(Badge, { size: "small", variant: deviceKindVariant(d.device), fill: "soft", label: d.device }), 메타(d) && _jsx("span", { className: "panel-row__device-detail body-3", children: 메타(d) })] }), _jsx("span", { className: "panel-row__device-name body-2b", children: d.productName || d.brand }), _jsx("span", { className: "panel-row__device-date caption-1", children: t('customers.device.wornSince', { date: `${d.wornYear}.${d.wornMonth}.${d.wornDay}` }) })] })) : (_jsx("span", { className: "panel-row__device-empty body-3", children: t('customers.device.notRegistered', '미등록') }))] }, side));
|
|
162
|
-
}) }));
|
|
163
|
-
}
|
|
25
|
+
export { PanelRow, PanelRowDevice } from './PanelRow'; // 갈라 나간 뒤에도 이 경로로 나간다
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import type { ReactNode, KeyboardEvent } from 'react';
|
|
2
|
+
import type { DeviceEntry, DeviceSide } from './HearingDeviceInfo';
|
|
3
|
+
import './Panel.css';
|
|
4
|
+
/**
|
|
5
|
+
* 패널 **한 줄** — `Panel` 이 담는 그릇이라면 이쪽은 그 안에 서는 줄이다.
|
|
6
|
+
*
|
|
7
|
+
* `Panel.tsx` 에서 갈라 나왔다. 한 파일에 컴포넌트 넷(Panel · PanelGrid · PanelRow ·
|
|
8
|
+
* PanelRowDevice)과 타입 열다섯이 함께 있었는데, 그릇을 고치러 들어온 사람이 줄의
|
|
9
|
+
* 12갈래 판별 합집합을 지나야 했다. 이 저장소가 이미 쓰는 방식이다
|
|
10
|
+
* (`trainingStepAudioPlayerTypes.ts` · `audioPlaybackRules.ts`).
|
|
11
|
+
*
|
|
12
|
+
* `Panel.tsx` 가 이것들을 다시 내보내므로 `@ihabdevteam/core/components/Panel` 에서
|
|
13
|
+
* 가져가던 곳은 그대로 돌아간다.
|
|
14
|
+
*/
|
|
15
|
+
type Base = {
|
|
16
|
+
label?: string;
|
|
17
|
+
sub?: string;
|
|
18
|
+
onClick?: () => void;
|
|
19
|
+
required?: boolean;
|
|
20
|
+
error?: string;
|
|
21
|
+
align?: 'horizontal' | 'vertical';
|
|
22
|
+
useFullWidth?: boolean;
|
|
23
|
+
icon?: ReactNode;
|
|
24
|
+
};
|
|
25
|
+
export type PanelRowToggle = Base & {
|
|
26
|
+
variant: 'toggle';
|
|
27
|
+
checked: boolean;
|
|
28
|
+
onChange: (v: boolean) => void;
|
|
29
|
+
};
|
|
30
|
+
export type PanelRowSegmented = Base & {
|
|
31
|
+
variant: 'segmented';
|
|
32
|
+
options: {
|
|
33
|
+
key: string;
|
|
34
|
+
label: string;
|
|
35
|
+
}[];
|
|
36
|
+
value: string;
|
|
37
|
+
onChange: (v: string) => void;
|
|
38
|
+
size?: 32 | 40 | 48;
|
|
39
|
+
};
|
|
40
|
+
export type DropdownItem = {
|
|
41
|
+
options: {
|
|
42
|
+
value: string;
|
|
43
|
+
label: string;
|
|
44
|
+
}[];
|
|
45
|
+
value: string;
|
|
46
|
+
onChange: (v: string) => void;
|
|
47
|
+
placeholder?: string;
|
|
48
|
+
/**
|
|
49
|
+
* 이 드롭다운 하나의 이름. 미지정 시 "행 라벨 + placeholder" 로 조합한다
|
|
50
|
+
* (생년월일 + 년 → "생년월일 년").
|
|
51
|
+
*/
|
|
52
|
+
ariaLabel?: string;
|
|
53
|
+
};
|
|
54
|
+
export type PanelRowDropdown = Base & {
|
|
55
|
+
variant: 'dropdown';
|
|
56
|
+
options?: {
|
|
57
|
+
value: string;
|
|
58
|
+
label: string;
|
|
59
|
+
}[];
|
|
60
|
+
value?: string;
|
|
61
|
+
onChange?: (v: string) => void;
|
|
62
|
+
dropdowns?: DropdownItem[];
|
|
63
|
+
};
|
|
64
|
+
export type ButtonItem = {
|
|
65
|
+
label: ReactNode;
|
|
66
|
+
btnVariant?: string;
|
|
67
|
+
btnSize?: number;
|
|
68
|
+
disabled?: boolean;
|
|
69
|
+
onClick: () => void;
|
|
70
|
+
};
|
|
71
|
+
export type PanelRowButton = Base & {
|
|
72
|
+
variant: 'button';
|
|
73
|
+
buttonLabel?: ReactNode;
|
|
74
|
+
btnVariant?: string;
|
|
75
|
+
btnSize?: number;
|
|
76
|
+
disabled?: boolean;
|
|
77
|
+
onClick?: () => void;
|
|
78
|
+
buttons?: ButtonItem[];
|
|
79
|
+
};
|
|
80
|
+
export type PanelRowText = Base & {
|
|
81
|
+
variant: 'text';
|
|
82
|
+
value: string;
|
|
83
|
+
valueColor?: string;
|
|
84
|
+
};
|
|
85
|
+
export type PanelRowInput = Base & {
|
|
86
|
+
variant: 'input';
|
|
87
|
+
value: string;
|
|
88
|
+
onChange?: (v: string) => void;
|
|
89
|
+
onKeyDown?: (e: KeyboardEvent<HTMLInputElement | HTMLTextAreaElement>) => void;
|
|
90
|
+
/** Enter로 확정(제출 등). TextField가 IME 조합 중 Enter는 걸러 주므로 한글 입력에서도 안전하다. */
|
|
91
|
+
onEnter?: () => void;
|
|
92
|
+
placeholder?: string;
|
|
93
|
+
inputMode?: 'none' | 'text' | 'decimal' | 'numeric' | 'tel' | 'search' | 'email' | 'url';
|
|
94
|
+
readOnly?: boolean;
|
|
95
|
+
disabled?: boolean;
|
|
96
|
+
type?: 'text' | 'email' | 'password' | 'number' | 'tel' | 'url';
|
|
97
|
+
autoComplete?: string;
|
|
98
|
+
helperText?: string;
|
|
99
|
+
/** 최대 글자 수 — 설정 시 입력 제한 + 박스 안쪽 글자수 카운터 표시 */
|
|
100
|
+
maxLength?: number;
|
|
101
|
+
/** 글자수 카운터 표시 여부 (maxLength 설정 시, 기본 true) */
|
|
102
|
+
showCount?: boolean;
|
|
103
|
+
/** 여러 줄 입력(textarea) 모드 */
|
|
104
|
+
multiline?: boolean;
|
|
105
|
+
/** multiline 모드 기본 행 수 */
|
|
106
|
+
rows?: number;
|
|
107
|
+
};
|
|
108
|
+
export type PanelRowCustom = Base & {
|
|
109
|
+
variant: 'custom';
|
|
110
|
+
children: ReactNode;
|
|
111
|
+
};
|
|
112
|
+
/** 라벨/우측 분리 없이 설명 문단을 통으로 표시하는 행 */
|
|
113
|
+
export type PanelRowDescription = {
|
|
114
|
+
variant: 'description';
|
|
115
|
+
children: ReactNode;
|
|
116
|
+
};
|
|
117
|
+
/** 빈 상태 표현 — Panel의 `empty` prop이 내부적으로 이 variant로 렌더한다. 단독으로도 사용 가능. */
|
|
118
|
+
export type PanelRowEmpty = {
|
|
119
|
+
variant: 'empty';
|
|
120
|
+
children: ReactNode;
|
|
121
|
+
};
|
|
122
|
+
/** 로딩 스피너 1개만 중앙에 표시 — Panel의 `loading` prop이 내부적으로 이 variant로 렌더한다. */
|
|
123
|
+
export type PanelRowLoading = {
|
|
124
|
+
variant: 'loading';
|
|
125
|
+
};
|
|
126
|
+
export type PanelRowChildren = {
|
|
127
|
+
label?: ReactNode;
|
|
128
|
+
sub?: string;
|
|
129
|
+
onClick?: () => void;
|
|
130
|
+
children?: ReactNode;
|
|
131
|
+
align?: 'horizontal' | 'vertical';
|
|
132
|
+
icon?: ReactNode;
|
|
133
|
+
};
|
|
134
|
+
export type PanelRowProps = PanelRowToggle | PanelRowSegmented | PanelRowDropdown | PanelRowButton | PanelRowText | PanelRowInput | PanelRowCustom | PanelRowDescription | PanelRowEmpty | PanelRowLoading | PanelRowChildren;
|
|
135
|
+
export declare function PanelRow(props: PanelRowProps): import("react").JSX.Element;
|
|
136
|
+
export type { DeviceSide };
|
|
137
|
+
export interface PanelRowDeviceProps {
|
|
138
|
+
devices: {
|
|
139
|
+
좌: DeviceEntry | null;
|
|
140
|
+
우: DeviceEntry | null;
|
|
141
|
+
};
|
|
142
|
+
/** 있으면 각 쪽 이름 옆에 '착용 해제'가 선다 — 그 쪽의 '지금 착용중' 만 내린다(이력은 남는다) */
|
|
143
|
+
onRelease?: (side: DeviceSide) => void;
|
|
144
|
+
}
|
|
145
|
+
export declare function PanelRowDevice({ devices, onRelease }: PanelRowDeviceProps): import("react").JSX.Element;
|