@ihabdevteam/core 0.188.0 → 0.190.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.
Files changed (28) hide show
  1. package/CHANGELOG.md +69 -0
  2. package/dist/components.cjs.js +180 -180
  3. package/dist/components.esm.css +3 -3
  4. package/dist/hooks.cjs.js +1 -1
  5. package/dist/i18n.cjs.js +1 -1
  6. package/dist/ihabdevteam-core.cjs.js +186 -186
  7. package/dist/ihabdevteam-core.d.ts +161 -4
  8. package/dist/packages/core/src/components/ChartSection.css +18 -3
  9. package/dist/packages/core/src/components/ChartSection.d.ts +12 -1
  10. package/dist/packages/core/src/components/ChartSection.js +3 -2
  11. package/dist/packages/core/src/components/LineChart.css +52 -0
  12. package/dist/packages/core/src/components/LineChart.d.ts +84 -0
  13. package/dist/packages/core/src/components/LineChart.js +59 -0
  14. package/dist/packages/core/src/components/Popup/Popup.css +38 -0
  15. package/dist/packages/core/src/components/Popup/Popup.d.ts +16 -0
  16. package/dist/packages/core/src/components/Popup/Popup.js +5 -2
  17. package/dist/packages/core/src/components/SectionHeadFloat.css +25 -0
  18. package/dist/packages/core/src/components/SectionHeadFloat.d.ts +28 -0
  19. package/dist/packages/core/src/components/SectionHeadFloat.js +54 -0
  20. package/dist/packages/core/src/components/WordTable.css +10 -0
  21. package/dist/packages/core/src/components/WordTable.d.ts +17 -2
  22. package/dist/packages/core/src/components/WordTable.js +13 -2
  23. package/dist/packages/core/src/components.d.ts +4 -0
  24. package/dist/packages/core/src/components.js +2 -0
  25. package/dist/packages/core/src/version.d.ts +1 -1
  26. package/dist/packages/core/src/version.js +1 -1
  27. package/dist/utils.cjs.js +1 -1
  28. package/package.json +1 -1
@@ -1088,7 +1088,7 @@ export declare interface ChartPoint {
1088
1088
  * 공유하도록 두 컴포넌트가 내부적으로 사용한다.
1089
1089
  * @public
1090
1090
  */
1091
- export declare function ChartSection({ title, titleClassName, icon, disc, headRight, headSegment, headMenuItems, collapseBelow, onClick, ariaLabel, tooltip, totalNum, totalUnit, totalLabel, subRow, subRowAlign, className, inGrid, children }: ChartSectionProps): JSX.Element;
1091
+ export declare function ChartSection({ title, titleClassName, icon, disc, headRight, headFloat, headSegment, headMenuItems, collapseBelow, onClick, ariaLabel, tooltip, totalNum, totalUnit, totalLabel, subRow, subRowAlign, className, inGrid, children }: ChartSectionProps): JSX.Element;
1092
1092
 
1093
1093
  /** @public */
1094
1094
  export declare interface ChartSectionProps {
@@ -1105,6 +1105,17 @@ export declare interface ChartSectionProps {
1105
1105
  headRight?: ReactNode | ((state: {
1106
1106
  collapsed: boolean;
1107
1107
  }) => ReactNode);
1108
+ /**
1109
+ * 머리 오른쪽에 **띄우는** 단추 줄 — `headRight` 와 달리 흐름에서 빠진다.
1110
+ *
1111
+ * 40px 짜리 단추를 제목 줄에 함께 세우면 제목 줄이 그만큼 커져 카드마다 머리 높이가 달라진다.
1112
+ * 띄우면 제목은 제 높이로 서고, 제목·곁줄은 이 줄의 **잰 폭만큼** 자리를 비운다. 좁은 화면
1113
+ * (600px 이하)에서는 흐름으로 되돌아가 제목 아래에 선다. 자세한 것은 `SectionHeadFloat`.
1114
+ *
1115
+ * 고르개·상세보기처럼 **제목과 한 줄로 서야 하는 것**은 `headRight` 가 맞다. 이쪽은 '숙제 보내기'
1116
+ * 처럼 카드 전체에 대고 누르는 단추 자리다.
1117
+ */
1118
+ headFloat?: ReactNode;
1108
1119
  /** 머리의 보기 고르개. 카드가 좁으면 '⋯' 한 단추로 접히고, 그 메뉴에 칸들이 들어간다. */
1109
1120
  headSegment?: ChartHeadSegment;
1110
1121
  /** 접힌 '⋯' 메뉴에서 칸들 **뒤** 에 구분선과 함께 붙는 항목(상세보기 등) */
@@ -1606,7 +1617,7 @@ export declare function copyText(text: string): Promise<boolean>;
1606
1617
  *
1607
1618
  * 값은 손으로 적고 `test/coreVersion.test.ts` 가 package.json 과 어긋나면 운다.
1608
1619
  */
1609
- export declare const CORE_VERSION = "0.188.0";
1620
+ export declare const CORE_VERSION = "0.190.0";
1610
1621
 
1611
1622
  /** @public */
1612
1623
  declare const coreDefault: Record<string, unknown>;
@@ -3165,6 +3176,93 @@ export declare const LEVEL_MAX = 9;
3165
3176
  /** @public */
3166
3177
  export declare const levelToPct: (level: number) => number;
3167
3178
 
3179
+ /** 점 위에 잠깐 머물면 뜨는 뜸 — 지나가는 마우스에 툴팁이 번쩍이지 않을 만큼만 짧게 @public */
3180
+ export declare const LINE_CHART_HOVER_DELAY_MS = 120;
3181
+
3182
+ /**
3183
+ * **작은 선 그래프** — 시간·조건에 따라 값이 어떻게 흐르는지를 그린다.
3184
+ * 세로는 0~100%로 고정이고, 가로 범위는 부르는 쪽이 준다.
3185
+ *
3186
+ * **제 상자를 재어 배율 1 로 그린다.** `viewBox` 를 상자보다 좁게 두면 SVG 가 통째로 늘어나
3187
+ * **글자까지 커진다**(말귀 실측 1.22배 — 사용자 지적 2026-09-17 "이 그래프가 폰트가 크게 보이는게
3188
+ * 스케일 되서 그런건가?"). 잰 픽셀을 그대로 쓰면 CSS 에 적은 12px 이 화면에서도 12px 이다.
3189
+ * 못 재는 자리(첫 렌더·ResizeObserver 없는 틀)에서는 `fallbackWidth` 로 그린다 — 늘어날 뿐 안 깨진다.
3190
+ *
3191
+ * **면 칠하기·짚는 자리처럼 그 화면에만 있는 말은 `overlay` 로 덧그린다** — 자(`x`·`y`)를 받으므로
3192
+ * 좌표 셈을 다시 하지 않는다.
3193
+ * @public
3194
+ */
3195
+ export declare function LineChart({ points, range, ticks, axisTitle, color, height, fallbackWidth, insets, overlay, ariaLabel, className, }: LineChartProps): JSX.Element;
3196
+
3197
+ /** @public */
3198
+ export declare interface LineChartPoint {
3199
+ /** 가로 자리 — `range` 안의 값 */
3200
+ x: number;
3201
+ /** 세로 자리 — 0~100(%) */
3202
+ pct: number;
3203
+ /** 속을 비워 그린다(믿기 이른 값 등). 비운 점은 **선에서도 뺀다** */
3204
+ hollow?: boolean;
3205
+ /** 마우스를 얹으면 뜰 글. 없으면 그 점에는 안 뜬다 */
3206
+ description?: string;
3207
+ }
3208
+
3209
+ /** @public */
3210
+ export declare interface LineChartProps {
3211
+ /** 그릴 점들 — 비면 축과 눈금만 선다 */
3212
+ points?: LineChartPoint[];
3213
+ /** 가로 범위 `[처음, 끝]` — 어떤 축인지(조건·주차·문항 수…)는 부르는 쪽이 정한다 */
3214
+ range: [number, number];
3215
+ /** 가로 눈금 — 안 주면 가로 글자가 없다 */
3216
+ ticks?: {
3217
+ x: number;
3218
+ label: string;
3219
+ }[];
3220
+ /**
3221
+ * 세로축 이름 — **안 주면 아무것도 안 그린다**(코어는 부르는 쪽의 사전을 모른다).
3222
+ *
3223
+ * **재는 것이 정답률이 아니면 반드시 준다.** 이름이 없으면 보는 사람이 정답률로 읽는다
3224
+ * (말귀 실측: 순서화는 '전부 맞힘' 비율인데 정답률로 읽혀 "문장별로는 정답률이 낮은데 왜
3225
+ * 전체 정답률은 높아?" 라는 물음이 나왔다 — 2026-09-17). 늘 같은 말이 붙는 화면이라면
3226
+ * 감싸개에서 기본값을 들어 주는 편이 낫다.
3227
+ */
3228
+ axisTitle?: ReactNode;
3229
+ /** 선·점 색. 안 주면 주 색 */
3230
+ color?: string;
3231
+ /** 그림 높이(px) — 배율이 1 이라 이 수가 곧 화면 높이다 */
3232
+ height?: number;
3233
+ /** 제 상자를 **못 잴 때만** 쓰는 대비 폭(첫 렌더·시험 틀) */
3234
+ fallbackWidth?: number;
3235
+ /** 축 안쪽 여백 — 왼쪽은 "100%" 가 들어갈 만큼이 기본이다 */
3236
+ insets?: Partial<{
3237
+ left: number;
3238
+ right: number;
3239
+ top: number;
3240
+ bottom: number;
3241
+ }>;
3242
+ /** 그림 위에 덧그릴 것 — 자를 받아 SVG 조각을 돌려준다(면 칠하기·짚는 자리 같은 **그 화면의 말**) */
3243
+ overlay?: (scales: LineChartScales) => ReactNode;
3244
+ /** 낭독기가 읽을 글 — 안 주면 점들의 `description` 을 한 줄로 잇는다 */
3245
+ ariaLabel?: string;
3246
+ className?: string;
3247
+ }
3248
+
3249
+ /** 그림 칸 안에서 값을 픽셀 자리로 옮기는 자 — `overlay` 가 이것으로 제 것을 덧그린다 @public */
3250
+ export declare interface LineChartScales {
3251
+ /** 가로 값 → x 픽셀 */
3252
+ x: (v: number) => number;
3253
+ /** 0~100 → y 픽셀 */
3254
+ y: (pct: number) => number;
3255
+ /** 그림이 그려지는 네모(축 안쪽) */
3256
+ plot: {
3257
+ left: number;
3258
+ right: number;
3259
+ top: number;
3260
+ bottom: number;
3261
+ width: number;
3262
+ height: number;
3263
+ };
3264
+ }
3265
+
3168
3266
  /** @public */
3169
3267
  export declare function ListenStep({ _stepIndex, _totalSteps, ...rest }: StepWrapperProps): JSX.Element;
3170
3268
 
@@ -4613,6 +4711,22 @@ export declare interface PopupProps {
4613
4711
  };
4614
4712
  /** 커스텀 footer 래퍼에 추가할 클래스 (정렬·간격 override용) */
4615
4713
  footerClassName?: string;
4714
+ /**
4715
+ * 바닥 줄 **왼쪽 덩이** — 보조 단추(아이콘 단추 여럿)가 서는 자리.
4716
+ *
4717
+ * `footerLayout: 'between'` 은 **직계 자식만** 양끝으로 민다. 그래서 "왼쪽 여럿 · 오른쪽 여럿" 을
4718
+ * 만들려면 부르는 쪽이 덩이 둘을 손수 지어야 했고, 그 규칙이 창마다 한 벌씩 생겼다(말귀 다섯 쌍,
4719
+ * 쌍마다 한 글자도 안 달랐다 — 2026-09-24). 바닥 줄의 자리 잡기는 **창의 일**이라 여기로 들인다.
4720
+ *
4721
+ * 덩이 안쪽 간격은 코어가 정한다 — 왼쪽은 촘촘히(`--spacing-xs`), 오른쪽은 한 눈금 넓게(`--spacing-s`),
4722
+ * 두 덩이 사이는 그보다 넓게(`--spacing-l`). 그래야 좁아져 줄이 바뀌어도 **두 덩이가 섞여 보이지 않는다.**
4723
+ * 좁으면 덩이째 줄을 바꾸고 오른쪽 덩이는 그 줄에서도 오른쪽 끝에 선다.
4724
+ *
4725
+ * 둘 중 하나만 줘도 된다. 주면 `footer`·`buttons` 보다 이쪽이 그려진다.
4726
+ */
4727
+ footerStart?: ReactNode | ((opts: PopupOptions) => ReactNode);
4728
+ /** 바닥 줄 **오른쪽 덩이** — 주 단추가 서는 자리. 자세한 것은 `footerStart`. */
4729
+ footerEnd?: ReactNode | ((opts: PopupOptions) => ReactNode);
4616
4730
  /** 바닥 줄의 가로 배치 — `'center'`(기본) · `'fill'` · `'between'`.
4617
4731
  *
4618
4732
  * - `'center'`: 지금까지와 같다(가운데 모음).
@@ -5861,6 +5975,34 @@ export declare interface SectionHeaderProps {
5861
5975
  /** @public */
5862
5976
  export declare type SectionHeaderTone = 'dark' | 'light';
5863
5977
 
5978
+ /**
5979
+ * **카드 머리 오른쪽에 띄우는 단추 줄.**
5980
+ *
5981
+ * 흐름에서 빼는 까닭은 자리가 아니라 **높이**다 — 40px 짜리 단추를 제목 줄에 함께 세우면
5982
+ * 제목 줄이 그만큼 커져 카드마다 머리 높이가 달라진다. 띄우면 제목은 제 높이로 선다.
5983
+ *
5984
+ * 세 가지를 이 조각이 맡는다. 앱마다 손으로 하다 보니 같은 규칙이 여러 벌이 됐다(말귀 두 벌 —
5985
+ * `top/right: 19px` 라는 **카드 안쪽 여백**을 베껴 적고 있었다. 그 수를 아는 쪽은 카드다).
5986
+ *
5987
+ * 1. **자리** — 카드 안쪽 여백에 맞춰 띄운다(`--card-pad`, 기본 19px). 카드가 여백을 고치면 함께 움직인다.
5988
+ * 2. **좁은 화면(600px 이하)에서 흐름으로 되돌린다** — 띄운 채로 두면 제목과 그 아랫줄을 덮는다.
5989
+ * 문턱이 600 인 것은 이 줄이 대개 250px 안팎이라, 그 아래에서는 제목과 나란히 설 자리가 없어서다.
5990
+ * 3. **제 폭을 재어 알린다** — 띄운 줄은 흐름에서 빠져 제목·곁줄을 덮으므로 그만큼 비워 둬야 하는데,
5991
+ * 부르는 쪽이 눈대중으로 `padding-right: 260px` 같은 수를 적고 있었다. 잰 폭을 부모에
5992
+ * `--section-head-float-w` 로 적으므로, 비울 자리는 `padding-right: calc(var(--section-head-float-w, -12px) + 12px)`
5993
+ * 한 줄이면 된다(안 띄운 카드에서는 0 이 된다). 코어 카드(`manager-section`)는 이미 그렇게 해 둔다.
5994
+ *
5995
+ * 기준점은 **부모가 준다** — 카드에 `position: relative` 가 있어야 한다(코어 카드에는 있다).
5996
+ * @public
5997
+ */
5998
+ export declare function SectionHeadFloat({ children, className }: SectionHeadFloatProps): JSX.Element;
5999
+
6000
+ /** @public */
6001
+ export declare interface SectionHeadFloatProps {
6002
+ children?: ReactNode;
6003
+ className?: string;
6004
+ }
6005
+
5864
6006
  /** @public */
5865
6007
  export declare interface SectionProps {
5866
6008
  type?: SectionType;
@@ -9969,8 +10111,23 @@ export declare interface WordTableProps {
9969
10111
  };
9970
10112
  /** 페이지 번호 위치 — 'none'이면 미표시 (기본 'right') */
9971
10113
  pageNumberPosition?: PageNumberPosition;
9972
- /** 컨테이너 높이에도 맞춰 축소한다(세로 용지가 넘쳐 잘리는 것 방지). 부모가 높이 제한 박스여야 한다. */
9973
- fitHeight?: boolean;
10114
+ /**
10115
+ * 정해진 세로 자리에 맞춰 줄인다(세로 용지가 넘쳐 잘리는 것 방지).
10116
+ *
10117
+ * - **수(px)** — 그 높이의 **담는 틀을 표가 스스로** 두른다. 표는 그 안에서 가로·세로 가운데에
10118
+ * 선다. 부르는 쪽은 "이만큼 자리에 맞춰 줄여" 한 마디면 된다.
10119
+ * - **`{ wide, narrow }`** — 좁은 화면(767px 이하)에서는 `narrow` 를 쓴다. 좁으면 **폭이 먼저
10120
+ * 걸려** 표가 가로에 맞춰 줄고 상자 아래가 비므로, 상자를 그만큼 낮춰 빈 자리를 없앤다
10121
+ * (말귀 실측 375px: 320px 상자에 183px 표 — 137px 이 비었다).
10122
+ * - **`true`** — 틀을 두르지 않는다. **부모가 높이 제한 박스여야 한다**(예전 동작).
10123
+ *
10124
+ * 앱마다 이 틀을 손수 만들다 보니 같은 규칙이 여러 벌이 됐다(말귀 두 벌, 주석 문장까지 같았다)
10125
+ * — 틀을 여기로 들인 까닭이다(2026-09-24).
10126
+ */
10127
+ fitHeight?: boolean | number | {
10128
+ wide: number;
10129
+ narrow: number;
10130
+ };
9974
10131
  }
9975
10132
 
9976
10133
  /**
@@ -12,7 +12,12 @@
12
12
  /* 테두리 1px 만큼 padding 을 1px 줄인다(20 → 19). 전역 box-sizing:border-box 는
13
13
  **정한 폭**에만 테두리를 넣어 줄 뿐 높이가 auto 인 카드는 위아래 2px 커지고,
14
14
  안쪽 내용도 1px 씩 안으로 밀린다. 이렇게 두면 바깥 크기와 내용 자리가 전과 같다. */
15
- padding: 19px;
15
+ /* 안쪽 여백을 변수로도 내보낸다 — 머리 오른쪽에 띄우는 줄(`SectionHeadFloat`)이 이 값에
16
+ 맞춰 서야 제목 줄과 윗선이 맞는다. 부르는 쪽이 그 수를 베껴 적지 않게 한다(2026-09-24). */
17
+ --card-pad: 19px;
18
+ padding: var(--card-pad);
19
+ /* 머리 오른쪽에 띄우는 줄(`SectionHeadFloat`)의 기준점 — 없으면 창 구석으로 날아간다 */
20
+ position: relative;
16
21
  margin-bottom: 14px;
17
22
  /* 차트 카드 — inline 나열 가능하도록 최소 폭만 지정(최대 폭 제한 없음) */
18
23
  min-width: 320px;
@@ -37,6 +42,13 @@
37
42
  /* 머리 바로 아래가 합계 줄이라 12px 은 떠 보인다 — 한 눈금 줄인다 */
38
43
  margin-bottom: var(--spacing-xs);
39
44
  }
45
+ /* **띄운 줄이 있으면 그만큼 비워 둔다.** 띄운 줄은 흐름에서 빠져 제목·곁줄을 덮는다.
46
+ `SectionHeadFloat` 이 잰 제 폭을 카드에 적어 두므로 눈대중으로 수를 적을 일이 없다 —
47
+ 안 띄운 카드에서는 -12 + 12 = 0 이라 아무 일도 안 일어난다(2026-09-24). */
48
+ .manager-section__title-row,
49
+ .manager-section__disc {
50
+ padding-right: calc(var(--section-head-float-w, -12px) + 12px);
51
+ }
40
52
  /* 제목은 줄어들 수 있어야 한다 — min-width 기본값(auto)이면 글자 길이만큼 버텨 넘친다 */
41
53
  .manager-section__title {
42
54
  min-width: 0;
@@ -106,7 +118,9 @@
106
118
  flex-wrap: nowrap;
107
119
  }
108
120
  .manager-section {
109
- padding: 17px; /* 18 − 테두리 1px */
121
+ /* 띄운 줄도 이 값에 맞춰 선다 — 여백은 `--card-pad` 한 군데서 정한다 */
122
+ --card-pad: 17px; /* 18 − 테두리 1px */
123
+ padding: var(--card-pad);
110
124
  /* 기본 규칙의 min-width:320px는 "차트 카드를 가로로 나란히 놓을 수 있게" 잡아둔
111
125
  하한인데, 이 폭부터는 카드가 한 장씩 세로로 쌓여 그 이유가 사라진다. 그대로
112
126
  두면 320px 기기에서 카드가 부모(280px)를 40px 넘쳐 오른쪽이 잘렸다.
@@ -207,7 +221,8 @@
207
221
  패딩·헤더 높이·간격을 StatChart(.manager-stat-card)와 동일하게 맞춰, 같은 그리드 행에 나란히
208
222
  놓였을 때 숫자 세로 위치가 어긋나지 않게 한다 */
209
223
  .manager-section--compact {
210
- padding: 19px; /* 20 − 테두리 1px */
224
+ --card-pad: 19px; /* 20 − 테두리 1px */
225
+ padding: var(--card-pad);
211
226
  min-width: 160px;
212
227
  }
213
228
  .manager-section--compact .manager-section__head {
@@ -50,6 +50,17 @@ export interface ChartSectionProps {
50
50
  headRight?: ReactNode | ((state: {
51
51
  collapsed: boolean;
52
52
  }) => ReactNode);
53
+ /**
54
+ * 머리 오른쪽에 **띄우는** 단추 줄 — `headRight` 와 달리 흐름에서 빠진다.
55
+ *
56
+ * 40px 짜리 단추를 제목 줄에 함께 세우면 제목 줄이 그만큼 커져 카드마다 머리 높이가 달라진다.
57
+ * 띄우면 제목은 제 높이로 서고, 제목·곁줄은 이 줄의 **잰 폭만큼** 자리를 비운다. 좁은 화면
58
+ * (600px 이하)에서는 흐름으로 되돌아가 제목 아래에 선다. 자세한 것은 `SectionHeadFloat`.
59
+ *
60
+ * 고르개·상세보기처럼 **제목과 한 줄로 서야 하는 것**은 `headRight` 가 맞다. 이쪽은 '숙제 보내기'
61
+ * 처럼 카드 전체에 대고 누르는 단추 자리다.
62
+ */
63
+ headFloat?: ReactNode;
53
64
  /** 머리의 보기 고르개. 카드가 좁으면 '⋯' 한 단추로 접히고, 그 메뉴에 칸들이 들어간다. */
54
65
  headSegment?: ChartHeadSegment;
55
66
  /** 접힌 '⋯' 메뉴에서 칸들 **뒤** 에 구분선과 함께 붙는 항목(상세보기 등) */
@@ -105,4 +116,4 @@ export interface ChartSectionProps {
105
116
  * 공유하도록 두 컴포넌트가 내부적으로 사용한다.
106
117
  * @public
107
118
  */
108
- export default function ChartSection({ title, titleClassName, icon, disc, headRight, headSegment, headMenuItems, collapseBelow, onClick, ariaLabel, tooltip, totalNum, totalUnit, totalLabel, subRow, subRowAlign, className, inGrid, children }: ChartSectionProps): import("react").JSX.Element;
119
+ export default function ChartSection({ title, titleClassName, icon, disc, headRight, headFloat, headSegment, headMenuItems, collapseBelow, onClick, ariaLabel, tooltip, totalNum, totalUnit, totalLabel, subRow, subRowAlign, className, inGrid, children }: ChartSectionProps): import("react").JSX.Element;
@@ -4,6 +4,7 @@ import './ChartSection.css';
4
4
  import { useCallback, useEffect, useRef, useState } from 'react';
5
5
  import { useTranslation } from 'react-i18next';
6
6
  import Icon from './Icon';
7
+ import SectionHeadFloat from './SectionHeadFloat';
7
8
  import Button from './Button';
8
9
  import SegmentedControl from './SegmentedControl';
9
10
  import Tooltip from './Tooltip/Tooltip';
@@ -28,7 +29,7 @@ export function ChartTotal({ label, children }) {
28
29
  * 공유하도록 두 컴포넌트가 내부적으로 사용한다.
29
30
  * @public
30
31
  */
31
- export default function ChartSection({ title, titleClassName = 'h3', icon, disc, headRight, headSegment, headMenuItems, collapseBelow = 420, onClick, ariaLabel, tooltip, totalNum, totalUnit, totalLabel, subRow, subRowAlign = 'start', className, inGrid = false, children }) {
32
+ export default function ChartSection({ title, titleClassName = 'h3', icon, disc, headRight, headFloat, headSegment, headMenuItems, collapseBelow = 420, onClick, ariaLabel, tooltip, totalNum, totalUnit, totalLabel, subRow, subRowAlign = 'start', className, inGrid = false, children }) {
32
33
  const { t } = useTranslation();
33
34
  const secRef = useRef(null);
34
35
  /* 설명 툴팁 — StatChart 와 같은 결(카드 사각형 기준, 포인터가 나가거나 초점이 빠지면 닫힘).
@@ -113,5 +114,5 @@ export default function ChartSection({ title, titleClassName = 'h3', icon, disc,
113
114
  bottom: 설명자리.bottom,
114
115
  width: 설명자리.width,
115
116
  height: 설명자리.height,
116
- }, text: tooltip })), _jsxs("div", { className: "manager-section__head", children: [_jsxs("div", { className: "manager-section__title-row", children: [icon && _jsx(Icon, { icon: icon, label: typeof title === 'string' ? title : undefined, className: "manager-section__icon" }), _jsx("div", { className: ['manager-section__title', titleClassName].filter(Boolean).join(' '), children: title })] }), 머리오른쪽, disc && _jsx("div", { className: "manager-section__disc body-3", children: disc })] }), (수줄 || subRow) && (_jsxs("div", { className: ['barchart-sub-row', subRowAlign !== 'start' && `barchart-sub-row--${subRowAlign}`].filter(Boolean).join(' '), children: [수줄, subRow] })), children, 메뉴자리 && (_jsx(Tooltip, { anchorRect: 메뉴자리, onClose: 메뉴닫기, ariaLabel: headSegment?.ariaLabel ?? t('tooltip.menu', '메뉴'), children: _jsx(MenuTooltip, { columns: [{ items: 메뉴항목 }], onClose: 메뉴닫기, size: "small" }) }))] }));
117
+ }, text: tooltip })), headFloat && _jsx(SectionHeadFloat, { children: headFloat }), _jsxs("div", { className: "manager-section__head", children: [_jsxs("div", { className: "manager-section__title-row", children: [icon && _jsx(Icon, { icon: icon, label: typeof title === 'string' ? title : undefined, className: "manager-section__icon" }), _jsx("div", { className: ['manager-section__title', titleClassName].filter(Boolean).join(' '), children: title })] }), 머리오른쪽, disc && _jsx("div", { className: "manager-section__disc body-3", children: disc })] }), (수줄 || subRow) && (_jsxs("div", { className: ['barchart-sub-row', subRowAlign !== 'start' && `barchart-sub-row--${subRowAlign}`].filter(Boolean).join(' '), children: [수줄, subRow] })), children, 메뉴자리 && (_jsx(Tooltip, { anchorRect: 메뉴자리, onClose: 메뉴닫기, ariaLabel: headSegment?.ariaLabel ?? t('tooltip.menu', '메뉴'), children: _jsx(MenuTooltip, { columns: [{ items: 메뉴항목 }], onClose: 메뉴닫기, size: "small" }) }))] }));
117
118
  }
@@ -0,0 +1,52 @@
1
+ /* 작은 선 그래프 — 까닭과 쓰는 법은 `LineChart.tsx` 의 글에.
2
+ **글자 크기를 여기 px 로 적는 것이 뜻이 있으려면** 그림이 배율 1 이어야 한다(그래서 제 상자를 잰다). */
3
+ .line-chart {
4
+ width: 100%;
5
+ height: auto;
6
+ display: block;
7
+ overflow: visible;
8
+ }
9
+
10
+ .line-chart__axis-title {
11
+ font-size: 12px;
12
+ fill: var(--color-gray);
13
+ }
14
+
15
+ .line-chart__tick {
16
+ font-size: 12px;
17
+ fill: var(--color-gray);
18
+ }
19
+
20
+ .line-chart__grid {
21
+ stroke: var(--color-whiteLine);
22
+ stroke-width: 1;
23
+ }
24
+
25
+ .line-chart__line {
26
+ stroke-width: 2;
27
+ stroke-linejoin: round;
28
+ stroke-linecap: round;
29
+ }
30
+
31
+ /* 겨누는 자리 — 보이지 않지만 손이 닿는다 */
32
+ .line-chart__dot-hit {
33
+ fill: transparent;
34
+ }
35
+
36
+ .line-chart__dot-main {
37
+ stroke-width: 1.5;
38
+ }
39
+ /* 속 빈 점 — 믿기 이른 값. 채움은 여기서 준다(선 색은 부르는 쪽이 정하므로 인라인이다) */
40
+ .line-chart__dot-main.is-hollow {
41
+ fill: var(--color-white);
42
+ }
43
+
44
+ /* 점에 얹으면 뜨는 말풍선 — 좁은 카드 안에서 줄이 잘리지 않게 */
45
+ .line-chart__tip {
46
+ max-width: 220px;
47
+ white-space: normal;
48
+ }
49
+
50
+ .line-chart__dot {
51
+ cursor: default;
52
+ }
@@ -0,0 +1,84 @@
1
+ import './LineChart.css';
2
+ import type { ReactNode } from 'react';
3
+ /** 점 위에 잠깐 머물면 뜨는 뜸 — 지나가는 마우스에 툴팁이 번쩍이지 않을 만큼만 짧게 @public */
4
+ export declare const LINE_CHART_HOVER_DELAY_MS = 120;
5
+ /** @public */
6
+ export interface LineChartPoint {
7
+ /** 가로 자리 — `range` 안의 값 */
8
+ x: number;
9
+ /** 세로 자리 — 0~100(%) */
10
+ pct: number;
11
+ /** 속을 비워 그린다(믿기 이른 값 등). 비운 점은 **선에서도 뺀다** */
12
+ hollow?: boolean;
13
+ /** 마우스를 얹으면 뜰 글. 없으면 그 점에는 안 뜬다 */
14
+ description?: string;
15
+ }
16
+ /** 그림 칸 안에서 값을 픽셀 자리로 옮기는 자 — `overlay` 가 이것으로 제 것을 덧그린다 @public */
17
+ export interface LineChartScales {
18
+ /** 가로 값 → x 픽셀 */
19
+ x: (v: number) => number;
20
+ /** 0~100 → y 픽셀 */
21
+ y: (pct: number) => number;
22
+ /** 그림이 그려지는 네모(축 안쪽) */
23
+ plot: {
24
+ left: number;
25
+ right: number;
26
+ top: number;
27
+ bottom: number;
28
+ width: number;
29
+ height: number;
30
+ };
31
+ }
32
+ /** @public */
33
+ export interface LineChartProps {
34
+ /** 그릴 점들 — 비면 축과 눈금만 선다 */
35
+ points?: LineChartPoint[];
36
+ /** 가로 범위 `[처음, 끝]` — 어떤 축인지(조건·주차·문항 수…)는 부르는 쪽이 정한다 */
37
+ range: [number, number];
38
+ /** 가로 눈금 — 안 주면 가로 글자가 없다 */
39
+ ticks?: {
40
+ x: number;
41
+ label: string;
42
+ }[];
43
+ /**
44
+ * 세로축 이름 — **안 주면 아무것도 안 그린다**(코어는 부르는 쪽의 사전을 모른다).
45
+ *
46
+ * **재는 것이 정답률이 아니면 반드시 준다.** 이름이 없으면 보는 사람이 정답률로 읽는다
47
+ * (말귀 실측: 순서화는 '전부 맞힘' 비율인데 정답률로 읽혀 "문장별로는 정답률이 낮은데 왜
48
+ * 전체 정답률은 높아?" 라는 물음이 나왔다 — 2026-09-17). 늘 같은 말이 붙는 화면이라면
49
+ * 감싸개에서 기본값을 들어 주는 편이 낫다.
50
+ */
51
+ axisTitle?: ReactNode;
52
+ /** 선·점 색. 안 주면 주 색 */
53
+ color?: string;
54
+ /** 그림 높이(px) — 배율이 1 이라 이 수가 곧 화면 높이다 */
55
+ height?: number;
56
+ /** 제 상자를 **못 잴 때만** 쓰는 대비 폭(첫 렌더·시험 틀) */
57
+ fallbackWidth?: number;
58
+ /** 축 안쪽 여백 — 왼쪽은 "100%" 가 들어갈 만큼이 기본이다 */
59
+ insets?: Partial<{
60
+ left: number;
61
+ right: number;
62
+ top: number;
63
+ bottom: number;
64
+ }>;
65
+ /** 그림 위에 덧그릴 것 — 자를 받아 SVG 조각을 돌려준다(면 칠하기·짚는 자리 같은 **그 화면의 말**) */
66
+ overlay?: (scales: LineChartScales) => ReactNode;
67
+ /** 낭독기가 읽을 글 — 안 주면 점들의 `description` 을 한 줄로 잇는다 */
68
+ ariaLabel?: string;
69
+ className?: string;
70
+ }
71
+ /**
72
+ * **작은 선 그래프** — 시간·조건에 따라 값이 어떻게 흐르는지를 그린다.
73
+ * 세로는 0~100%로 고정이고, 가로 범위는 부르는 쪽이 준다.
74
+ *
75
+ * **제 상자를 재어 배율 1 로 그린다.** `viewBox` 를 상자보다 좁게 두면 SVG 가 통째로 늘어나
76
+ * **글자까지 커진다**(말귀 실측 1.22배 — 사용자 지적 2026-09-17 "이 그래프가 폰트가 크게 보이는게
77
+ * 스케일 되서 그런건가?"). 잰 픽셀을 그대로 쓰면 CSS 에 적은 12px 이 화면에서도 12px 이다.
78
+ * 못 재는 자리(첫 렌더·ResizeObserver 없는 틀)에서는 `fallbackWidth` 로 그린다 — 늘어날 뿐 안 깨진다.
79
+ *
80
+ * **면 칠하기·짚는 자리처럼 그 화면에만 있는 말은 `overlay` 로 덧그린다** — 자(`x`·`y`)를 받으므로
81
+ * 좌표 셈을 다시 하지 않는다.
82
+ * @public
83
+ */
84
+ export default function LineChart({ points, range, ticks, axisTitle, color, height, fallbackWidth, insets, overlay, ariaLabel, className, }: LineChartProps): import("react").JSX.Element;
@@ -0,0 +1,59 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { useCallback, useEffect, useState } from 'react';
4
+ import { cx } from '../utils/cx';
5
+ import './LineChart.css';
6
+ import Tooltip from './Tooltip/Tooltip';
7
+ import { useDelayedHoverAnchor } from '../hooks/useHoverAnchor';
8
+ /** 점 위에 잠깐 머물면 뜨는 뜸 — 지나가는 마우스에 툴팁이 번쩍이지 않을 만큼만 짧게 @public */
9
+ export const LINE_CHART_HOVER_DELAY_MS = 120;
10
+ const 기본여백 = { left: 38, right: 16, top: 26, bottom: 24 };
11
+ /**
12
+ * **작은 선 그래프** — 시간·조건에 따라 값이 어떻게 흐르는지를 그린다.
13
+ * 세로는 0~100%로 고정이고, 가로 범위는 부르는 쪽이 준다.
14
+ *
15
+ * **제 상자를 재어 배율 1 로 그린다.** `viewBox` 를 상자보다 좁게 두면 SVG 가 통째로 늘어나
16
+ * **글자까지 커진다**(말귀 실측 1.22배 — 사용자 지적 2026-09-17 "이 그래프가 폰트가 크게 보이는게
17
+ * 스케일 되서 그런건가?"). 잰 픽셀을 그대로 쓰면 CSS 에 적은 12px 이 화면에서도 12px 이다.
18
+ * 못 재는 자리(첫 렌더·ResizeObserver 없는 틀)에서는 `fallbackWidth` 로 그린다 — 늘어날 뿐 안 깨진다.
19
+ *
20
+ * **면 칠하기·짚는 자리처럼 그 화면에만 있는 말은 `overlay` 로 덧그린다** — 자(`x`·`y`)를 받으므로
21
+ * 좌표 셈을 다시 하지 않는다.
22
+ * @public
23
+ */
24
+ export default function LineChart({ points = [], range, ticks = [], axisTitle, color, height = 156, fallbackWidth = 320, insets, overlay, ariaLabel, className, }) {
25
+ /* **제 상자를 재어 그 픽셀 수를 그림 칸으로 쓴다.** `width: 100%` 는 그대로라 viewBox 를 바꿔도
26
+ 가로가 다시 바뀌지 않는다(되먹임이 없다). */
27
+ const [잰폭, set잰폭] = useState(null);
28
+ const [칸, set칸] = useState(null);
29
+ useEffect(() => {
30
+ if (!칸)
31
+ return;
32
+ const 재기 = (w) => set잰폭(Math.round(w) || null);
33
+ 재기(칸.getBoundingClientRect().width);
34
+ if (typeof ResizeObserver === 'undefined')
35
+ return;
36
+ const 지켜보기 = new ResizeObserver(([e]) => 재기(e.contentRect.width));
37
+ 지켜보기.observe(칸);
38
+ return () => { 지켜보기.disconnect(); };
39
+ }, [칸]);
40
+ const { payload: 짚은글, anchorRect: 짚은자리, openAfterDelay, close: 닫기 } = useDelayedHoverAnchor(LINE_CHART_HOVER_DELAY_MS);
41
+ const 얹힘 = useCallback((글, el) => {
42
+ if (글)
43
+ openAfterDelay(글, el.getBoundingClientRect());
44
+ }, [openAfterDelay]);
45
+ const W = 잰폭 ?? fallbackWidth, H = height;
46
+ const { left: 왼, right: 오른, top: 위, bottom: 아래 } = { ...기본여백, ...insets };
47
+ const [처음, 끝] = range;
48
+ /* 범위는 부르는 쪽이 적어도 한 칸 폭으로 준다(끝 > 처음) — 같으면 0 으로 나누므로 왼쪽 끝에 몰아 둔다 */
49
+ const x = (v) => (끝 === 처음 ? 왼 : 왼 + ((W - 왼 - 오른) * (v - 처음)) / (끝 - 처음));
50
+ const y = (pct) => 위 + ((H - 위 - 아래) * (100 - pct)) / 100;
51
+ const 자 = {
52
+ x, y,
53
+ plot: { left: 왼, right: W - 오른, top: 위, bottom: H - 아래, width: Math.max(0, W - 왼 - 오른), height: Math.max(0, H - 위 - 아래) },
54
+ };
55
+ /* 선은 **믿을 점만** 잇는다 — 속 빈 점까지 이으면 "이만큼 봤다" 는 말이 흐려진다 */
56
+ const 이을점 = points.filter(p => !p.hollow);
57
+ const 색 = color ?? 'var(--color-main)';
58
+ return (_jsxs(_Fragment, { children: [_jsxs("svg", { ref: set칸, className: cx('line-chart', className), viewBox: `0 0 ${W} ${H}`, role: "img", "aria-label": ariaLabel ?? points.map(p => p.description).filter(Boolean).join(', '), children: [axisTitle && _jsx("text", { className: "line-chart__axis-title", x: 0, y: 12, children: axisTitle }), [0, 100].map(v => (_jsxs("g", { children: [_jsx("line", { className: "line-chart__grid", x1: 왼, x2: W - 오른, y1: y(v), y2: y(v) }), _jsxs("text", { className: "line-chart__tick", x: 왼 - 6, y: y(v) + 4, textAnchor: "end", children: [v, "%"] })] }, v))), overlay?.(자), ticks.map(tk => (_jsx("text", { className: "line-chart__tick", x: x(tk.x), y: H - 5, textAnchor: "middle", children: tk.label }, tk.x))), 이을점.length > 1 && (_jsx("polyline", { className: "line-chart__line", fill: "none", stroke: 색, points: 이을점.map(p => `${x(p.x)},${y(p.pct)}`).join(' ') })), points.map(p => (_jsxs("g", { className: "line-chart__dot", onPointerEnter: e => 얹힘(p.description, e.currentTarget), onPointerLeave: 닫기, children: [_jsx("circle", { className: "line-chart__dot-hit", cx: x(p.x), cy: y(p.pct), r: 12 }), _jsx("circle", { className: cx('line-chart__dot-main', p.hollow && 'is-hollow'), cx: x(p.x), cy: y(p.pct), r: 4, stroke: 색, fill: p.hollow ? undefined : 색 })] }, p.x)))] }), 짚은글 != null && (_jsx(Tooltip, { anchorRect: 짚은자리, offset: 10, closeOnOutsideClick: false, className: "line-chart__tip", role: "tooltip", ariaLabel: 짚은글, children: 짚은글 }))] }));
59
+ }
@@ -110,6 +110,44 @@
110
110
  `flex: 1` 의 기본 바닥(min-width: auto)은 글자 길이만큼 버틴다 — 0 으로 내려 줄이 바뀌기 전에
111
111
  먼저 줄어들게 하고, 그래도 모자라면 위의 `flex-wrap` 이 아랫줄로 내린다. */
112
112
  .popup-footer--buttons > * { max-width: 200px; flex: 1 1 auto; min-width: 0; }
113
+
114
+ /* **바닥 줄의 두 덩이**(`footerStart` · `footerEnd`) — 왼쪽 보조, 오른쪽 주 단추.
115
+ 창마다 한 벌씩 베껴 적히던 규칙이다(말귀 다섯 쌍, 쌍마다 한 글자도 안 달랐다 — 2026-09-24).
116
+
117
+ 간격은 **안쪽보다 사이가 넓다**(xs·s < l). 좁아져 줄이 바뀌어도 두 덩이가 한 덩이로 섞여
118
+ 보이지 않게 하는 것이 이 차이의 일이다. 줄은 바꾸되(넘쳐 잘리는 것보다 낫다 — 0.185.0 에서
119
+ 바닥이 잘려 못 누르던 자리를 겪었다) 오른쪽 덩이는 새 줄에서도 오른쪽 끝에 선다. */
120
+ .popup-footer--bar {
121
+ justify-content: space-between;
122
+ gap: var(--spacing-l);
123
+ flex-wrap: wrap;
124
+ }
125
+ .popup-footer__start {
126
+ display: flex;
127
+ align-items: center;
128
+ gap: var(--spacing-xs);
129
+ flex-wrap: wrap;
130
+ /* 글자가 길어도 줄이 바뀌기 전에 먼저 줄어들게 — 기본값(auto)은 글자 길이만큼 버틴다 */
131
+ min-width: 0;
132
+ }
133
+ .popup-footer__end {
134
+ display: flex;
135
+ align-items: center;
136
+ gap: var(--spacing-s);
137
+ flex-wrap: wrap;
138
+ /* 왼쪽이 비었거나 줄이 바뀌어도 오른쪽 끝에 선다 */
139
+ margin-left: auto;
140
+ justify-content: flex-end;
141
+ min-width: 0;
142
+ }
143
+ /* **덩이째 줄을 바꾸고도 넘치면 안쪽에서 한 번 더 바꾼다.** 단추는 제 최소 폭(120px)으로 버티므로
144
+ 덩이에 `flex-shrink: 0` 을 두면 그대로 창 밖으로 나가 잘린다 — 375px 에서 오른쪽 단추 둘이
145
+ 25px 넘쳐 잘렸다(실측 2026-09-24). 0.185.0 에서 바닥이 잘려 못 누르던 그 꼴이다.
146
+ 최소 폭을 0 으로 내려, 줄이 바뀌든 줄어들든 **잘리지는 않게** 한다. */
147
+ .popup-footer__start > *,
148
+ .popup-footer__end > * {
149
+ min-width: 0;
150
+ }
113
151
  .popup--closing { opacity: 0; transition: opacity 260ms ease }
114
152
 
115
153
  @media (max-width: 767px) {
@@ -52,6 +52,22 @@ export interface PopupProps {
52
52
  };
53
53
  /** 커스텀 footer 래퍼에 추가할 클래스 (정렬·간격 override용) */
54
54
  footerClassName?: string;
55
+ /**
56
+ * 바닥 줄 **왼쪽 덩이** — 보조 단추(아이콘 단추 여럿)가 서는 자리.
57
+ *
58
+ * `footerLayout: 'between'` 은 **직계 자식만** 양끝으로 민다. 그래서 "왼쪽 여럿 · 오른쪽 여럿" 을
59
+ * 만들려면 부르는 쪽이 덩이 둘을 손수 지어야 했고, 그 규칙이 창마다 한 벌씩 생겼다(말귀 다섯 쌍,
60
+ * 쌍마다 한 글자도 안 달랐다 — 2026-09-24). 바닥 줄의 자리 잡기는 **창의 일**이라 여기로 들인다.
61
+ *
62
+ * 덩이 안쪽 간격은 코어가 정한다 — 왼쪽은 촘촘히(`--spacing-xs`), 오른쪽은 한 눈금 넓게(`--spacing-s`),
63
+ * 두 덩이 사이는 그보다 넓게(`--spacing-l`). 그래야 좁아져 줄이 바뀌어도 **두 덩이가 섞여 보이지 않는다.**
64
+ * 좁으면 덩이째 줄을 바꾸고 오른쪽 덩이는 그 줄에서도 오른쪽 끝에 선다.
65
+ *
66
+ * 둘 중 하나만 줘도 된다. 주면 `footer`·`buttons` 보다 이쪽이 그려진다.
67
+ */
68
+ footerStart?: ReactNode | ((opts: PopupOptions) => ReactNode);
69
+ /** 바닥 줄 **오른쪽 덩이** — 주 단추가 서는 자리. 자세한 것은 `footerStart`. */
70
+ footerEnd?: ReactNode | ((opts: PopupOptions) => ReactNode);
55
71
  /** 바닥 줄의 가로 배치 — `'center'`(기본) · `'fill'` · `'between'`.
56
72
  *
57
73
  * - `'center'`: 지금까지와 같다(가운데 모음).
@@ -16,7 +16,7 @@ const ANIM_MS = 260;
16
16
  /** @public */
17
17
  export default function Popup(props) {
18
18
  const { t } = useTranslation();
19
- const { title, subtitle, headerActions, headerSegment, children, buttons, footer, footerClassName = "", footerLayout = 'center', onClose, closeDisabled = false, beforeClose, closeOnBackdrop = true, className = "", ariaLabel, useTitle = true, inline = false, size, layer = 'modal', height, fillBody = false, resetScrollKey } = props || {};
19
+ const { title, subtitle, headerActions, headerSegment, children, buttons, footer, footerStart, footerEnd, footerClassName = "", footerLayout = 'center', onClose, closeDisabled = false, beforeClose, closeOnBackdrop = true, className = "", ariaLabel, useTitle = true, inline = false, size, layer = 'modal', height, fillBody = false, resetScrollKey } = props || {};
20
20
  const [closing, setClosing] = useState(false);
21
21
  // 닫힘 애니메이션 중(260ms)엔 버튼이 시각적으로만 흐려질 뿐 클릭은 계속 가능해,
22
22
  // 그 사이 같은 버튼을 연타하면 cb(확인/완료 콜백)가 여러 번 예약되어 중복 실행된다.
@@ -212,7 +212,10 @@ export default function Popup(props) {
212
212
  e.preventDefault(); }, children: _jsxs("div", { ref: panelRef, className: `popup-panel${size ? ` popup-panel--${size}` : ''}${height != null ? ' popup-panel--has-height' : ''}${fillBody ? ' popup-panel--fill-body' : ''}`,
213
213
  /* 높이는 **CSS 변수로** 넘긴다 — 인라인 height 로 박으면 앱의 미디어 규칙(좁은 화면만 다른 높이)이 못 이겼다.
214
214
  max-height(CSS)가 height 보다 이기므로 화면보다 크게 줘도 화면에 맞춰진다 */
215
- style: height != null ? { '--popup-height': typeof height === 'number' ? `${height}px` : height } : undefined, tabIndex: -1, role: inline ? undefined : 'dialog', "aria-modal": inline ? undefined : true, "aria-labelledby": !inline && useTitle && title ? titleId : undefined, "aria-label": !inline && !(useTitle && title) ? (ariaLabel ?? (typeof title === 'string' ? title : undefined)) : undefined, children: [!useTitle && (headerActions || onClose) ? (_jsxs("div", { className: "popup-close-float", children: [headerActions, onClose && _jsx(Button, { htmlType: "button", size: 40, type: "dot", icon: "x", variant: "transparent-gray", useBorder: false, onClick: closeWithAnimation, disabled: closeDisabled, ariaLabel: t('common.close', '닫기') })] })) : (title || headerActions || onClose) ? (_jsxs("div", { className: `popup-header${subtitle ? ' popup-header--with-subtitle' : ''}`, children: [_jsxs("div", { className: "popup-header-main", children: [_jsx("span", { id: titleId, className: "popup-title popup-title--left h2", children: title }), subtitle && _jsx("span", { className: "popup-subtitle body-3", children: subtitle })] }), headerSegment && (_jsx("div", { className: "popup-header-segment", children: _jsx(SegmentedControl, { size: 40, ariaLabel: headerSegment.ariaLabel, options: headerSegment.options.map(o => ({ key: o.key, label: o.label, disabled: o.disabled })), value: headerSegment.value, onChange: headerSegment.onChange }) })), _jsxs("div", { className: "popup-header-actions", children: [headerActions, onClose && (_jsx(Button, { htmlType: "button", size: 40, type: "dot", icon: "x", variant: "transparent-gray", useBorder: false, onClick: closeWithAnimation, disabled: closeDisabled, ariaLabel: t('common.close', '닫기') }))] })] })) : null, _jsx("div", { className: "popup-content", ref: contentRef, tabIndex: contentScrollable ? 0 : undefined, children: _jsx("div", { className: "popup-content__inner", children: typeof children === 'function' ? children({ animateThen, closeWithAnimation }) : children }) }), buttons?.length ? (_jsx("div", { className: `popup-footer popup-footer--buttons${footerLayout !== 'center' ? ` popup-footer--${footerLayout}` : ''}`, children: buttons.map((btn, i) => (_jsx(Button, { htmlType: "button", size: 48, variant: btn.variant ?? 'default', label: btn.label, disabled: btn.disabled, onClick: () => btn.onClick?.({ animateThen, closeWithAnimation }) }, i))) })) : footer ? (_jsx("div", { className: `popup-footer${footerLayout !== 'center' ? ` popup-footer--${footerLayout}` : ''}${footerClassName ? ` ${footerClassName}` : ''}`, children: typeof footer === 'function' ? footer({ animateThen, closeWithAnimation }) : footer })) : null] }) }));
215
+ style: height != null ? { '--popup-height': typeof height === 'number' ? `${height}px` : height } : undefined, tabIndex: -1, role: inline ? undefined : 'dialog', "aria-modal": inline ? undefined : true, "aria-labelledby": !inline && useTitle && title ? titleId : undefined, "aria-label": !inline && !(useTitle && title) ? (ariaLabel ?? (typeof title === 'string' ? title : undefined)) : undefined, children: [!useTitle && (headerActions || onClose) ? (_jsxs("div", { className: "popup-close-float", children: [headerActions, onClose && _jsx(Button, { htmlType: "button", size: 40, type: "dot", icon: "x", variant: "transparent-gray", useBorder: false, onClick: closeWithAnimation, disabled: closeDisabled, ariaLabel: t('common.close', '닫기') })] })) : (title || headerActions || onClose) ? (_jsxs("div", { className: `popup-header${subtitle ? ' popup-header--with-subtitle' : ''}`, children: [_jsxs("div", { className: "popup-header-main", children: [_jsx("span", { id: titleId, className: "popup-title popup-title--left h2", children: title }), subtitle && _jsx("span", { className: "popup-subtitle body-3", children: subtitle })] }), headerSegment && (_jsx("div", { className: "popup-header-segment", children: _jsx(SegmentedControl, { size: 40, ariaLabel: headerSegment.ariaLabel, options: headerSegment.options.map(o => ({ key: o.key, label: o.label, disabled: o.disabled })), value: headerSegment.value, onChange: headerSegment.onChange }) })), _jsxs("div", { className: "popup-header-actions", children: [headerActions, onClose && (_jsx(Button, { htmlType: "button", size: 40, type: "dot", icon: "x", variant: "transparent-gray", useBorder: false, onClick: closeWithAnimation, disabled: closeDisabled, ariaLabel: t('common.close', '닫기') }))] })] })) : null, _jsx("div", { className: "popup-content", ref: contentRef, tabIndex: contentScrollable ? 0 : undefined, children: _jsx("div", { className: "popup-content__inner", children: typeof children === 'function' ? children({ animateThen, closeWithAnimation }) : children }) }), (footerStart || footerEnd) ? (
216
+ /* 왼쪽·오른쪽 덩이 — 빈 덩이도 그린다(자리를 지켜야 오른쪽이 끝에 선다).
217
+ `footerLayout` 은 보지 않는다: 이 줄 자체가 배치다. */
218
+ _jsxs("div", { className: `popup-footer popup-footer--bar${footerClassName ? ` ${footerClassName}` : ''}`, children: [_jsx("div", { className: "popup-footer__start", children: typeof footerStart === 'function' ? footerStart({ animateThen, closeWithAnimation }) : footerStart }), _jsx("div", { className: "popup-footer__end", children: typeof footerEnd === 'function' ? footerEnd({ animateThen, closeWithAnimation }) : footerEnd })] })) : buttons?.length ? (_jsx("div", { className: `popup-footer popup-footer--buttons${footerLayout !== 'center' ? ` popup-footer--${footerLayout}` : ''}`, children: buttons.map((btn, i) => (_jsx(Button, { htmlType: "button", size: 48, variant: btn.variant ?? 'default', label: btn.label, disabled: btn.disabled, onClick: () => btn.onClick?.({ animateThen, closeWithAnimation }) }, i))) })) : footer ? (_jsx("div", { className: `popup-footer${footerLayout !== 'center' ? ` popup-footer--${footerLayout}` : ''}${footerClassName ? ` ${footerClassName}` : ''}`, children: typeof footer === 'function' ? footer({ animateThen, closeWithAnimation }) : footer })) : null] }) }));
216
219
  if (inline || typeof document === 'undefined')
217
220
  return el;
218
221
  return createPortal(el, document.body);
@@ -0,0 +1,25 @@
1
+ /* 카드 머리 오른쪽에 띄우는 단추 줄 — 까닭과 쓰는 법은 `SectionHeadFloat.tsx` 의 글에. */
2
+ .section-head-float {
3
+ position: absolute;
4
+ /* 카드 안쪽 여백과 같은 값이라야 제목 줄과 윗선이 맞는다 — 그 수는 카드가 알고 있다 */
5
+ top: var(--card-pad, 19px);
6
+ right: var(--card-pad, 19px);
7
+ display: flex;
8
+ align-items: center;
9
+ justify-content: flex-end;
10
+ gap: var(--spacing-m);
11
+ /* 제목이 설 자리는 남긴다 */
12
+ max-width: 50%;
13
+ z-index: 1;
14
+ }
15
+
16
+ /* **좁으면 흐름으로 되돌린다** — 띄운 채로 두면 제목과 그 아랫줄을 덮고, 단추 이름도 두 줄로 갈린다.
17
+ 이 줄은 대개 250px 안팎이라 600px 아래에서는 제목과 나란히 설 자리가 없다. */
18
+ @media (max-width: 600px) {
19
+ .section-head-float {
20
+ position: static;
21
+ max-width: none;
22
+ margin-top: var(--spacing-m);
23
+ flex-wrap: wrap;
24
+ }
25
+ }