@ihabdevteam/core 0.12.3 → 0.13.1

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.
@@ -3889,7 +3889,7 @@ export declare interface ProgressBarProps {
3889
3889
  }
3890
3890
 
3891
3891
  /** @public */
3892
- export declare function ProgressIndicator({ max, index, inactiveBg, inactiveColor, transparent }: ProgressIndicatorProps): JSX.Element | null;
3892
+ export declare function ProgressIndicator({ max, index, inactiveBg, inactiveColor, transparent, variant, announce, ariaLabel }: ProgressIndicatorProps): JSX.Element | null;
3893
3893
 
3894
3894
  /** @public */
3895
3895
  export declare interface ProgressIndicatorProps {
@@ -3905,6 +3905,32 @@ export declare interface ProgressIndicatorProps {
3905
3905
  * inactiveBg·inactiveColor 를 직접 주면 그쪽이 이긴다.
3906
3906
  */
3907
3907
  transparent?: boolean;
3908
+ /**
3909
+ * 표시 방식.
3910
+ *
3911
+ * - `numbered` — 번호를 적은 52×28 칸. 기본값이다. 훈련 팝업처럼 "지금 몇
3912
+ * 번째인지" 를 글로 읽어야 하는 자리에 쓴다.
3913
+ * - `dot` — 번호 없는 8px 점. 지금 칸만 알약으로 늘어난다. 화면에서 멀리
3914
+ * 떨어져 보는 안내처럼, 진행이 있다는 것만 알리고 본문에 자리를 내주는
3915
+ * 자리에 쓴다. 좁은 화면에서도 접지 않는다 — 점은 이미 작다.
3916
+ */
3917
+ variant?: 'numbered' | 'dot';
3918
+ /**
3919
+ * 단계가 바뀔 때 낭독기가 스스로 알리게 한다(`role="status"`).
3920
+ *
3921
+ * 기본은 알리지 않는다 — 점 모드는 그림에 가까워 `role="img"` + 이름표로
3922
+ * 두는 편이 조용하다. 사용자가 넘기는 것이 아니라 화면이 저절로 넘어가는
3923
+ * 자리(검사 안내)에서는 켜는 편이 낫다.
3924
+ */
3925
+ announce?: boolean;
3926
+ /**
3927
+ * 이름표를 직접 준다. 안 주면 i18n 의 `progress.steps` 로 짓는다.
3928
+ *
3929
+ * i18n 을 쓰지 않는 앱에서는 그 값이 한글 기본값으로 떨어진다 —
3930
+ * 화면은 영문인데 이름표만 한글이 되는 자리가 실제로 있었다
3931
+ * (sound-localization-system 의 병원용 빌드).
3932
+ */
3933
+ ariaLabel?: string;
3908
3934
  }
3909
3935
 
3910
3936
  /** key → 실제 배경 컴포넌트. PromoBanner 등 사용처가 이걸로 렌더한다. 각 컴포넌트는 자신의
@@ -5466,7 +5492,7 @@ export declare type TabItem = {
5466
5492
  /** @public */
5467
5493
  export declare function Table<T extends {
5468
5494
  id?: string | number;
5469
- }>({ fields, headerGroups, items, renderRow, onRowClick, onRowMouseEnter, onRowMouseLeave, onRowDragStart, onRowDragEnd, draggingId, gridTemplateColumns, resizable, resizeMode, resizeStorageKey, size, title, count, collapsed, onToggleCollapse, headerActions, empty, loading, dragOver, onDragOver, onDragLeave, onDrop, renderExpandedRow, expandedId, }: TableProps<T>): JSX.Element;
5495
+ }>({ fields, headerGroups, items, renderRow, onRowClick, onRowMouseEnter, onRowMouseLeave, onRowDragStart, onRowDragEnd, draggingId, gridTemplateColumns, resizable, resizeMode, resizeStorageKey, size, title, icon, count, collapsed, onToggleCollapse, headerActions, empty, loading, dragOver, onDragOver, onDragLeave, onDrop, renderExpandedRow, expandedId, }: TableProps<T>): JSX.Element;
5470
5496
 
5471
5497
  /**
5472
5498
  * **다음에 쓸 표를 고른다.**
@@ -5608,6 +5634,13 @@ export declare type TableProps<T extends {
5608
5634
  size?: 'small' | 'medium' | 'large';
5609
5635
  /** 테이블 상단 그룹 헤더 제목. 지정하면 .table-header 영역이 렌더됩니다. */
5610
5636
  title?: ReactNode;
5637
+ /**
5638
+ * 제목 앞에 서는 lucide 아이콘 이름. Panel 의 `icon` 과 같은 자리다.
5639
+ *
5640
+ * 이 자리가 없어서 부르는 쪽이 아이콘을 `title` 노드 안에 넣고 있었다 —
5641
+ * 표와 판넬을 나란히 놓는 화면에서 둘의 머리 모양이 미묘하게 갈린다.
5642
+ */
5643
+ icon?: string;
5611
5644
  /** 제목 옆 항목 갯수 표시 */
5612
5645
  count?: number;
5613
5646
  /** 접힘 상태 — true면 필드/행 영역을 숨김. onToggleCollapse와 함께 사용 */
@@ -25,7 +25,7 @@ export default function Panel({ title, icon, disc, actions, size = 'medium', min
25
25
  style.maxWidth = typeof maxWidth === 'number' ? `${maxWidth}px` : maxWidth;
26
26
  if (labelWidth !== undefined)
27
27
  style['--panel-label-width'] = typeof labelWidth === 'number' ? `${labelWidth}px` : labelWidth;
28
- return (_jsxs("div", { className: cls, style: Object.keys(style).length ? style : undefined, children: [(title != null || disc != null) && (_jsxs("div", { className: cx('panel__header', disc != null ? 'panel__header--with-disc' : ''), children: [icon && (_jsx(Icon, { icon: icon, label: typeof title === 'string' ? title : undefined, className: "panel__icon" })), _jsxs("div", { className: "panel__title-group", children: [title != null && _jsx("h2", { className: "panel__title h5b", children: title }), disc != null && _jsx("p", { className: "panel__disc caption-1", children: disc })] }), actions && _jsx("div", { className: "panel__actions", children: actions })] })), loading ? (_jsx(PanelRow, { variant: "loading" })) : empty != null && (children == null || (Array.isArray(children) && children.length === 0)) ? (_jsx(PanelRow, { variant: "empty", children: empty })) : scrollable ? (_jsx("div", { className: "panel__body--scroll", children: children })) : children] }));
28
+ return (_jsxs("div", { className: cls, style: Object.keys(style).length ? style : undefined, children: [(title != null || disc != null) && (_jsxs("div", { className: cx('panel__header', disc != null ? 'panel__header--with-disc' : ''), children: [icon && (_jsx(Icon, { icon: icon, label: typeof title === 'string' ? title : undefined, className: "panel__icon" })), _jsxs("div", { className: "panel__title-group", children: [title != null && (_jsx("h2", { className: `panel__title ${disc != null ? 'body-3b' : 'h5b'}`, children: title })), disc != null && _jsx("p", { className: "panel__disc caption-1", children: disc })] }), actions && _jsx("div", { className: "panel__actions", children: actions })] })), loading ? (_jsx(PanelRow, { variant: "loading" })) : empty != null && (children == null || (Array.isArray(children) && children.length === 0)) ? (_jsx(PanelRow, { variant: "empty", children: empty })) : scrollable ? (_jsx("div", { className: "panel__body--scroll", children: children })) : children] }));
29
29
  }
30
30
  // ── PanelGrid ──────────────────────────────────────────────────────────────
31
31
  /** @public */
@@ -39,3 +39,32 @@
39
39
  height: 28px;
40
40
  }
41
41
  }
42
+
43
+ /* ── 점 모드 ────────────────────────────────────────────────────────────
44
+ 번호 없이 자리만 알린다. 지금 칸은 폭으로 갈린다 — 색만으로 가르면 색을
45
+ 구별하기 어려운 사람에게 네 칸이 같아 보인다. 폭이 바뀌는 동안 움직임이
46
+ 생기므로 전환을 짧게 두되, 움직임을 줄여 달라는 설정은 따른다. */
47
+ .progress-indicator--dot {
48
+ gap: var(--spacing-xs);
49
+ }
50
+
51
+ .progress-indicator-dot--dot {
52
+ width: 8px;
53
+ height: 8px;
54
+ border-radius: var(--radius-full);
55
+ background: var(--pi-inactive-bg, var(--color-darkWhite));
56
+ transition: width 0.2s ease-out, background 0.2s ease-out;
57
+ }
58
+
59
+ .progress-indicator-dot--dot.done {
60
+ background: color-mix(in srgb, var(--color-main) 40%, transparent);
61
+ }
62
+
63
+ .progress-indicator-dot--dot.active {
64
+ width: 24px;
65
+ background: var(--color-main);
66
+ }
67
+
68
+ @media (prefers-reduced-motion: reduce) {
69
+ .progress-indicator-dot--dot { transition: none; }
70
+ }
@@ -13,6 +13,32 @@ export interface ProgressIndicatorProps {
13
13
  * inactiveBg·inactiveColor 를 직접 주면 그쪽이 이긴다.
14
14
  */
15
15
  transparent?: boolean;
16
+ /**
17
+ * 표시 방식.
18
+ *
19
+ * - `numbered` — 번호를 적은 52×28 칸. 기본값이다. 훈련 팝업처럼 "지금 몇
20
+ * 번째인지" 를 글로 읽어야 하는 자리에 쓴다.
21
+ * - `dot` — 번호 없는 8px 점. 지금 칸만 알약으로 늘어난다. 화면에서 멀리
22
+ * 떨어져 보는 안내처럼, 진행이 있다는 것만 알리고 본문에 자리를 내주는
23
+ * 자리에 쓴다. 좁은 화면에서도 접지 않는다 — 점은 이미 작다.
24
+ */
25
+ variant?: 'numbered' | 'dot';
26
+ /**
27
+ * 단계가 바뀔 때 낭독기가 스스로 알리게 한다(`role="status"`).
28
+ *
29
+ * 기본은 알리지 않는다 — 점 모드는 그림에 가까워 `role="img"` + 이름표로
30
+ * 두는 편이 조용하다. 사용자가 넘기는 것이 아니라 화면이 저절로 넘어가는
31
+ * 자리(검사 안내)에서는 켜는 편이 낫다.
32
+ */
33
+ announce?: boolean;
34
+ /**
35
+ * 이름표를 직접 준다. 안 주면 i18n 의 `progress.steps` 로 짓는다.
36
+ *
37
+ * i18n 을 쓰지 않는 앱에서는 그 값이 한글 기본값으로 떨어진다 —
38
+ * 화면은 영문인데 이름표만 한글이 되는 자리가 실제로 있었다
39
+ * (sound-localization-system 의 병원용 빌드).
40
+ */
41
+ ariaLabel?: string;
16
42
  }
17
43
  /** @public */
18
- export default function ProgressIndicator({ max, index, inactiveBg, inactiveColor, transparent }: ProgressIndicatorProps): import("react").JSX.Element | null;
44
+ export default function ProgressIndicator({ max, index, inactiveBg, inactiveColor, transparent, variant, announce, ariaLabel }: ProgressIndicatorProps): import("react").JSX.Element | null;
@@ -1,4 +1,4 @@
1
- import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useTranslation } from 'react-i18next';
3
3
  import useMediaQuery from '../hooks/useMediaQuery';
4
4
  import './ProgressIndicator.css';
@@ -14,7 +14,7 @@ import './ProgressIndicator.css';
14
14
  */
15
15
  const TRANSPARENT_INACTIVE_BG = 'rgba(255, 255, 255, 0.16)';
16
16
  /** @public */
17
- export default function ProgressIndicator({ max, index, inactiveBg, inactiveColor, transparent }) {
17
+ export default function ProgressIndicator({ max, index, inactiveBg, inactiveColor, transparent, variant = 'numbered', announce = false, ariaLabel }) {
18
18
  const { t } = useTranslation();
19
19
  const isMobile = useMediaQuery('(max-width: 767px)');
20
20
  if (!Number.isInteger(max) || max <= 0)
@@ -23,7 +23,7 @@ export default function ProgressIndicator({ max, index, inactiveBg, inactiveColo
23
23
  /* 목록에 이름이 없어 낭독기가 "목록, 4개, 1, 2, 3, 4" 로만 읽었다 — 무엇의 목록인지,
24
24
  지금 어디인지 알 수 없다. 훈련 팝업에는 숫자 묶음이 여럿이라 더 헷갈린다.
25
25
  칸마다 붙은 aria-current 는 목록을 훑어야 닿으므로, **머리에서 바로 자리를 말한다.** */
26
- const groupLabel = t('progress.steps', {
26
+ const groupLabel = ariaLabel ?? t('progress.steps', {
27
27
  current: idx + 1, total: max, defaultValue: '진행 단계 {{current}} / {{total}}',
28
28
  });
29
29
  const rootStyle = {};
@@ -37,6 +37,13 @@ export default function ProgressIndicator({ max, index, inactiveBg, inactiveColo
37
37
  if (!inactiveColor)
38
38
  rootStyle['--pi-inactive-color'] = 'var(--color-white)';
39
39
  }
40
+ /* 점 모드 — 번호가 없으므로 칸마다 읽을 것이 없다. role="list" 로 두면
41
+ 낭독기가 "목록, 4개, 빈 항목…" 으로 읽어 도움이 되지 않는다. 그림 하나로
42
+ 묶고 이름표에 자리를 적는다. 화면이 스스로 넘어가는 자리에서는
43
+ announce 로 role="status" 를 켠다. */
44
+ if (variant === 'dot') {
45
+ return (_jsx("div", { className: "progress-indicator progress-indicator--dot", style: rootStyle, role: announce ? 'status' : 'img', "aria-label": groupLabel, children: Array.from({ length: max }).map((_, i) => (_jsx("span", { "aria-hidden": "true", className: `progress-indicator-dot--dot${i < idx ? ' done' : i === idx ? ' active' : ''}` }, i))) }));
46
+ }
40
47
  // 모바일: 현재 단계만 표시 (n / max 형태)
41
48
  if (isMobile) {
42
49
  return (_jsx("div", { className: "progress-indicator progress-indicator--mobile", style: rootStyle, role: "list", "aria-label": groupLabel, children: _jsxs("span", { role: "listitem", className: "progress-indicator-dot active body-3", children: [idx + 1, " / ", max] }) }));
@@ -30,6 +30,18 @@
30
30
  flex-shrink: 0;
31
31
  }
32
32
 
33
+ /* 제목 앞 아이콘 — Panel 머리의 아이콘과 같은 크기·같은 색. 표와 판넬을
34
+ 나란히 놓는 화면에서 두 머리가 같은 모양이 되도록. */
35
+ .table-header__icon {
36
+ width: 20px;
37
+ height: 20px;
38
+ flex-shrink: 0;
39
+ display: flex;
40
+ align-items: center;
41
+ justify-content: center;
42
+ color: var(--t-item-fg, var(--color-main));
43
+ }
44
+
33
45
  .table-header__title {
34
46
  color: var(--color-darkGray);
35
47
  }
@@ -66,6 +66,13 @@ export type TableProps<T extends {
66
66
  size?: 'small' | 'medium' | 'large';
67
67
  /** 테이블 상단 그룹 헤더 제목. 지정하면 .table-header 영역이 렌더됩니다. */
68
68
  title?: ReactNode;
69
+ /**
70
+ * 제목 앞에 서는 lucide 아이콘 이름. Panel 의 `icon` 과 같은 자리다.
71
+ *
72
+ * 이 자리가 없어서 부르는 쪽이 아이콘을 `title` 노드 안에 넣고 있었다 —
73
+ * 표와 판넬을 나란히 놓는 화면에서 둘의 머리 모양이 미묘하게 갈린다.
74
+ */
75
+ icon?: string;
69
76
  /** 제목 옆 항목 갯수 표시 */
70
77
  count?: number;
71
78
  /** 접힘 상태 — true면 필드/행 영역을 숨김. onToggleCollapse와 함께 사용 */
@@ -91,4 +98,4 @@ export type TableProps<T extends {
91
98
  /** @public */
92
99
  export default function Table<T extends {
93
100
  id?: string | number;
94
- }>({ fields, headerGroups, items, renderRow, onRowClick, onRowMouseEnter, onRowMouseLeave, onRowDragStart, onRowDragEnd, draggingId, gridTemplateColumns, resizable, resizeMode, resizeStorageKey, size, title, count, collapsed, onToggleCollapse, headerActions, empty, loading, dragOver, onDragOver, onDragLeave, onDrop, renderExpandedRow, expandedId, }: TableProps<T>): import("react").JSX.Element;
101
+ }>({ fields, headerGroups, items, renderRow, onRowClick, onRowMouseEnter, onRowMouseLeave, onRowDragStart, onRowDragEnd, draggingId, gridTemplateColumns, resizable, resizeMode, resizeStorageKey, size, title, icon, count, collapsed, onToggleCollapse, headerActions, empty, loading, dragOver, onDragOver, onDragLeave, onDrop, renderExpandedRow, expandedId, }: TableProps<T>): import("react").JSX.Element;
@@ -15,6 +15,7 @@ import { useState, useRef, useCallback, useEffect } from 'react';
15
15
  import ChevronUp from 'lucide-react/dist/esm/icons/chevron-up.mjs';
16
16
  import ChevronDown from 'lucide-react/dist/esm/icons/chevron-down.mjs';
17
17
  import Button from './Button';
18
+ import Icon from './Icon';
18
19
  import Loader from './Loader';
19
20
  function TableCell({ children, className, style }) {
20
21
  return _jsx("div", { className: className ? `table-cell ${className}` : 'table-cell', style: style, children: children });
@@ -60,7 +61,7 @@ function TableField({ children, onClick, className, sortDir, onResizeStart }) {
60
61
  return (_jsxs("div", { onClick: onClick, onKeyDown: onClick ? handleKeyDown : undefined, role: onClick ? 'button' : undefined, tabIndex: onClick ? 0 : undefined, "aria-sort": sortDir ? (sortDir === 'asc' ? 'ascending' : 'descending') : undefined, className: cx('table-field body-3', className, sortDir ? 'table-field--sorted' : '', onResizeStart ? 'table-field--resizable' : ''), children: [children, sortDir && _jsx(SortArrow, { dir: sortDir }), onResizeStart && (_jsx("span", { className: "table-field__resizer", onPointerDown: onResizeStart, onClick: e => e.stopPropagation(), "aria-hidden": "true" }))] }));
61
62
  }
62
63
  /** @public */
63
- export default function Table({ fields = [], headerGroups, items = [], renderRow, onRowClick, onRowMouseEnter, onRowMouseLeave, onRowDragStart, onRowDragEnd, draggingId, gridTemplateColumns, resizable = false, resizeMode = 'grow', resizeStorageKey, size = 'small', title, count, collapsed = false, onToggleCollapse, headerActions, empty, loading, dragOver = false, onDragOver, onDragLeave, onDrop, renderExpandedRow, expandedId, }) {
64
+ export default function Table({ fields = [], headerGroups, items = [], renderRow, onRowClick, onRowMouseEnter, onRowMouseLeave, onRowDragStart, onRowDragEnd, draggingId, gridTemplateColumns, resizable = false, resizeMode = 'grow', resizeStorageKey, size = 'small', title, icon, count, collapsed = false, onToggleCollapse, headerActions, empty, loading, dragOver = false, onDragOver, onDragLeave, onDrop, renderExpandedRow, expandedId, }) {
64
65
  const { t } = useTranslation();
65
66
  // ── 컬럼 폭 드래그 리사이즈 (resizable) ──
66
67
  const MIN_COL_W = 48;
@@ -183,7 +184,7 @@ export default function Table({ fields = [], headerGroups, items = [], renderRow
183
184
  return (_jsx("div", { className: `table-wrap table-wrap--${size}${dragOver ? ' table-wrap--dragover' : ''}${collapsed ? ' table-wrap--collapsed' : ''}`, style: gridStyle, onDragOver: onDragOver, onDragLeave: onDragLeave, onDrop: onDrop, children: _jsxs("div", { className: "table body-2", children: [hasHeader && (_jsxs("div", { className: "table-header", onClick: onToggleCollapse, role: onToggleCollapse ? 'button' : undefined, tabIndex: onToggleCollapse ? 0 : undefined, "aria-expanded": onToggleCollapse ? !collapsed : undefined, onKeyDown: onToggleCollapse ? (e) => { if (e.key === 'Enter' || e.key === ' ') {
184
185
  e.preventDefault();
185
186
  onToggleCollapse();
186
- } } : undefined, children: [onToggleCollapse && (_jsx(Button, { size: 40, type: "dot", variant: "transparent-gray", useBorder: false, icon: collapsed ? 'chevron-right' : 'chevron-down', ariaLabel: collapsed ? t('common.expand', '펼치기') : t('common.collapse', '접기'), className: "table-header__chevron" })), _jsx("span", { className: "table-header__title h5b", children: title }), count !== undefined && _jsx("span", { className: "table-header__count body-3b", children: count }), headerActions && (_jsx("span", { className: "table-header__actions", onClick: e => e.stopPropagation(), children: headerActions }))] })), !collapsed && (_jsxs(_Fragment, { children: [headerGroups && headerGroups.length > 0 && (_jsx("div", { className: "table-fields table-fields--groups", children: headerGroups.map((g, i) => (_jsx("div", { className: cx('table-field body-3', g.className), style: { gridColumn: `span ${g.span}` }, children: g.label }, i))) })), _jsx("div", { className: "table-fields", ref: fieldsRef, children: fields.map((f, i) => (_jsx(TableField, { onClick: f.onClick, className: f.className, sortDir: f.sortDir,
187
+ } } : undefined, children: [onToggleCollapse && (_jsx(Button, { size: 40, type: "dot", variant: "transparent-gray", useBorder: false, icon: collapsed ? 'chevron-right' : 'chevron-down', ariaLabel: collapsed ? t('common.expand', '펼치기') : t('common.collapse', '접기'), className: "table-header__chevron" })), icon && (_jsx(Icon, { icon: icon, label: typeof title === 'string' ? title : undefined, className: "table-header__icon" })), _jsx("span", { className: "table-header__title h5b", children: title }), count !== undefined && _jsx("span", { className: "table-header__count body-3b", children: count }), headerActions && (_jsx("span", { className: "table-header__actions", onClick: e => e.stopPropagation(), children: headerActions }))] })), !collapsed && (_jsxs(_Fragment, { children: [headerGroups && headerGroups.length > 0 && (_jsx("div", { className: "table-fields table-fields--groups", children: headerGroups.map((g, i) => (_jsx("div", { className: cx('table-field body-3', g.className), style: { gridColumn: `span ${g.span}` }, children: g.label }, i))) })), _jsx("div", { className: "table-fields", ref: fieldsRef, children: fields.map((f, i) => (_jsx(TableField, { onClick: f.onClick, className: f.className, sortDir: f.sortDir,
187
188
  /* fit 모드는 오른쪽 이웃에서 폭을 덜어 오므로, 덜어 올 컬럼이 없는 자리에는
188
189
  손잡이를 두지 않는다 — 두면 그 컬럼만 grow처럼 넓어져 표가 밀린다. */
189
190
  onResizeStart: resizable && i < fields.length - 1 && f.resizable !== false
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ihabdevteam/core",
3
- "version": "0.12.3",
3
+ "version": "0.13.1",
4
4
  "private": false,
5
5
  "main": "dist/ihabdevteam-core.cjs.js",
6
6
  "module": "dist/ihabdevteam-core.esm.js",
@@ -113,6 +113,7 @@
113
113
  "smoke-test": "node ./scripts/smoke-test.js",
114
114
  "audit:exports": "node ./scripts/audit-exports.cjs",
115
115
  "audit:docs": "node ./scripts/audit-docs.cjs",
116
+ "audit:props": "node ./scripts/audit-props-tables.cjs",
116
117
  "analyze:bundle": "node ./scripts/analyze-bundle.cjs",
117
118
  "pack:check": "bash ./scripts/publish-check.sh",
118
119
  "check:size": "size-limit",