@ihabdevteam/core 0.19.0 → 0.21.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.
@@ -5119,7 +5119,7 @@ export declare interface SliderProps {
5119
5119
  export declare function sortToValue(key: string, asc: boolean): string | undefined;
5120
5120
 
5121
5121
  /** @public */
5122
- export declare function SoundCompass({ speakers, size, viewT, activeSpeakerId, playedIds, selectedId, answerId, showFeedback, pulseSelected, pulseSelectedDuration, clickable, onSelectSpeaker, buttonSize, showIcon, shortcutAnchor, shortcutScale, scaleShortcut, }: SoundCompassProps): JSX.Element;
5122
+ export declare function SoundCompass({ speakers, size, viewT, activeSpeakerId, playedIds, selectedId, answerId, showFeedback, pulseSelected, pulseSelectedDuration, pulseScale, clickable, onSelectSpeaker, buttonSize, showIcon, shortcutAnchor, shortcutScale, scaleShortcut, }: SoundCompassProps): JSX.Element;
5123
5123
 
5124
5124
  export declare interface SoundCompassExam {
5125
5125
  /** `<SoundCompass {...bind} />` 로 펼쳐 넣는 제어 props */
@@ -5212,6 +5212,17 @@ export declare interface SoundCompassProps {
5212
5212
  * 상태를 알리는 것이라 짧을 까닭이 없다. 필요해지면 그때 연다.
5213
5213
  */
5214
5214
  pulseSelectedDuration?: number;
5215
+ /**
5216
+ * 울림이 퍼지는 최대 배율. 기본 2.8.
5217
+ *
5218
+ * 얼마나 멀리 퍼져야 알맞은지는 **스피커 사이 거리**에 달렸다. 촘촘한 배치에서는
5219
+ * 2.8 배가 이웃 버튼 앞까지 밀고 들어온다 — 소비 앱 실측: 버튼 지름 90px 이면
5220
+ * 울림 지름이 252px 인데 이웃과의 중심 거리가 261px 이었다.
5221
+ *
5222
+ * 되풀이하는 울림과 한 번짜리 울림에 함께 걸린다. 둘의 퍼짐이 달라야 할 까닭이
5223
+ * 아직 없다 — 필요해지면 그때 가른다.
5224
+ */
5225
+ pulseScale?: number;
5215
5226
  /** 스피커 클릭 가능 여부. 기본 false */
5216
5227
  clickable?: boolean;
5217
5228
  /** 스피커 선택 시 호출 */
@@ -5219,7 +5230,23 @@ export declare interface SoundCompassProps {
5219
5230
  /** 스피커 버튼 크기(px). 기본 40 */
5220
5231
  buttonSize?: number;
5221
5232
  /** 스피커 버튼에 아이콘을 표시할지 여부. false면 숫자만 가운데 표시. 기본 true */
5222
- showIcon?: boolean;
5233
+ /**
5234
+ * 버튼 안에 무엇을 그릴지.
5235
+ *
5236
+ * - `true` — 아이콘과 번호를 함께 (기본)
5237
+ * - `false` — 번호만
5238
+ * - `'answered'` — **답이 정해진 상태에서만 아이콘으로 갈아 끼운다.** 그때는
5239
+ * 번호를 그리지 않는다. 나머지(`idle`·`active`)는 `false` 와 같이 번호만이다.
5240
+ *
5241
+ * `'answered'` 가 가르는 선은 자의적이지 않다. `idle` 과 `active` 는 아이콘이
5242
+ * 둘 다 같은 `Volume2` 라 번호보다 더 말해 주는 것이 없다. `played`·`correct` 의
5243
+ * 체크와 `wrong` 의 가위표만이 번호가 못 하는 말을 한다 — 아이콘이 자리를
5244
+ * 차지할 값을 하는 자리만 바꿔 끼운다.
5245
+ *
5246
+ * 스피커를 번호로 부르는 화면에서 필요하다. `true` 로 켜면 고르지 않은 스피커까지
5247
+ * 아이콘이 붙고 번호가 함께 작아져(실측: 36px → 18px) 번호로 부르는 화면이 무너진다.
5248
+ */
5249
+ showIcon?: boolean | 'answered';
5223
5250
  /**
5224
5251
  * `speakers[].shortcut` 배지가 버튼의 어느 모서리에 붙을지. 기본 우상단.
5225
5252
  *
@@ -8,6 +8,31 @@
8
8
  flex-direction: column;
9
9
  align-items: center;
10
10
  width: 100%;
11
+
12
+ /* 상태 색은 **뿌리에** 둔다. 예전에는 버튼 안에 선언돼 있었는데, 울림(pulse)은
13
+ 버튼의 형제라 그 변수를 못 봤다 — 울림 색을 상태에 묶자마자 투명하게 나왔다
14
+ (실측: rgba(0,0,0,0)). 버튼과 울림이 함께 보려면 공통 조상에 있어야 한다.
15
+
16
+ **덮어쓸 때도 여기나 그 위에 얹어야 한다.** `.sound-compass__speaker-btn` 같은
17
+ 버튼 선택자 위에 얹으면 버튼만 바뀌고 울림은 안 따라온다 — 그 자리에서는
18
+ 조용히 어긋나므로, 색이 갈려 보이면 이것부터 보라. */
19
+ --scb-idle-bg: var(--color-deepWhite);
20
+ --scb-idle-border: var(--color-whiteLine);
21
+ --scb-idle-fg: var(--color-lightGray);
22
+ --scb-active-bg: var(--color-main);
23
+ --scb-active-border: var(--color-main);
24
+ --scb-active-fg: #ffffff;
25
+ --scb-played-bg: var(--color-sub);
26
+ --scb-played-border: var(--color-sub);
27
+ --scb-played-fg: #ffffff;
28
+ --scb-wrong-bg: #ef4444;
29
+ --scb-wrong-border: #ef4444;
30
+ --scb-wrong-fg: #ffffff;
31
+ /* 호버·포커스도 상태 색과 같은 방식으로 연다 — 소비자가 바꿔 끼울 수 있게. */
32
+ --scb-idle-hover-bg: var(--color-whiteLine);
33
+ --scb-idle-hover-border: var(--color-lightGray);
34
+ /* 호버의 글자색. 기본은 idle 과 같다 — 주지 않으면 예전 그대로다. */
35
+ --scb-idle-hover-fg: var(--scb-idle-fg);
11
36
  }
12
37
 
13
38
  .sound-compass__status {
@@ -43,23 +68,6 @@
43
68
  border-style: solid;
44
69
  transition: background-color .15s, border-color .15s, color .15s;
45
70
 
46
- --scb-idle-bg: var(--color-deepWhite);
47
- --scb-idle-border: var(--color-whiteLine);
48
- --scb-idle-fg: var(--color-lightGray);
49
- --scb-active-bg: var(--color-main);
50
- --scb-active-border: var(--color-main);
51
- --scb-active-fg: #ffffff;
52
- --scb-played-bg: var(--color-sub);
53
- --scb-played-border: var(--color-sub);
54
- --scb-played-fg: #ffffff;
55
- --scb-wrong-bg: #ef4444;
56
- --scb-wrong-border: #ef4444;
57
- --scb-wrong-fg: #ffffff;
58
- /* 호버·포커스도 상태 색과 같은 방식으로 연다 — 소비자가 바꿔 끼울 수 있게. */
59
- --scb-idle-hover-bg: var(--color-whiteLine);
60
- --scb-idle-hover-border: var(--color-lightGray);
61
- /* 호버의 글자색. 기본은 idle 과 같다 — 주지 않으면 예전 그대로다. */
62
- --scb-idle-hover-fg: var(--scb-idle-fg);
63
71
  --scb-focus-ring: var(--color-main);
64
72
  }
65
73
 
@@ -124,7 +132,10 @@
124
132
  @keyframes sound-compass-pulse {
125
133
  0% { transform: scale(0.95); opacity: 0; }
126
134
  8% { transform: scale(1.05); opacity: 0.45; }
127
- 100% { transform: scale(2.8); opacity: 0; }
135
+ /* 얼마나 멀리 퍼질지는 스피커 사이 거리에 달렸다 — 2.8 배는 촘촘한 배치에서
136
+ 이웃 버튼 앞까지 밀고 들어간다(소비 앱 실측: 버튼 90px 이면 울림 지름 252px,
137
+ 이웃과의 중심 거리 261px). keyframe 안의 var() 는 정적 값이라 잘 돈다. */
138
+ 100% { transform: scale(var(--sc-pulse-scale, 2.8)); opacity: 0; }
128
139
  }
129
140
 
130
141
  .sound-compass__pulse {
@@ -132,12 +143,23 @@
132
143
  top: 0; left: 0;
133
144
  width: 40px; height: 40px;
134
145
  border-radius: 50%;
135
- background: var(--color-main);
146
+ /* 색은 **그 순간 버튼 색**을 따른다. 아래 상태별 규칙이 정하고, 여기 것은
147
+ 상태를 못 붙였을 때의 바닥값이다. `--sc-pulse-color` 로 덮을 수 있다. */
148
+ background: var(--sc-pulse-color, var(--scb-active-bg));
136
149
  pointer-events: none;
137
150
  animation: sound-compass-pulse 1s cubic-bezier(0.33, 1, 0.68, 1) infinite;
138
151
  }
139
152
  /* 고른 순간을 알리는 울림은 한 번만 번진다. 마지막 프레임이 opacity 0 이라
140
153
  되풀이를 끊는 것만으로 그대로 사라진다. */
154
+ /* 울림 색이 버튼 색과 갈리면 청록 버튼에서 파란 울림이 번진다(소비 앱 실측:
155
+ 고른 직후 버튼은 --scb-played-bg 인데 울림은 --color-main 이었다). 상태를 그대로
156
+ 따라가게 두면 `--scb-*` 를 바꿔 끼운 소비자도 저절로 맞는다. */
157
+ .sound-compass__pulse--idle { background: var(--sc-pulse-color, var(--scb-idle-bg)); }
158
+ .sound-compass__pulse--active { background: var(--sc-pulse-color, var(--scb-active-bg)); }
159
+ .sound-compass__pulse--played,
160
+ .sound-compass__pulse--correct { background: var(--sc-pulse-color, var(--scb-played-bg)); }
161
+ .sound-compass__pulse--wrong { background: var(--sc-pulse-color, var(--scb-wrong-bg)); }
162
+
141
163
  .sound-compass__pulse--once {
142
164
  animation-iteration-count: 1;
143
165
  /* 고른 상태를 짧게만 잡아 두는 검사에서는 이 시간을 줄여야 울림이 안 끊긴다. */
@@ -56,6 +56,17 @@ export interface SoundCompassProps {
56
56
  * 상태를 알리는 것이라 짧을 까닭이 없다. 필요해지면 그때 연다.
57
57
  */
58
58
  pulseSelectedDuration?: number;
59
+ /**
60
+ * 울림이 퍼지는 최대 배율. 기본 2.8.
61
+ *
62
+ * 얼마나 멀리 퍼져야 알맞은지는 **스피커 사이 거리**에 달렸다. 촘촘한 배치에서는
63
+ * 2.8 배가 이웃 버튼 앞까지 밀고 들어온다 — 소비 앱 실측: 버튼 지름 90px 이면
64
+ * 울림 지름이 252px 인데 이웃과의 중심 거리가 261px 이었다.
65
+ *
66
+ * 되풀이하는 울림과 한 번짜리 울림에 함께 걸린다. 둘의 퍼짐이 달라야 할 까닭이
67
+ * 아직 없다 — 필요해지면 그때 가른다.
68
+ */
69
+ pulseScale?: number;
59
70
  /** 스피커 클릭 가능 여부. 기본 false */
60
71
  clickable?: boolean;
61
72
  /** 스피커 선택 시 호출 */
@@ -63,7 +74,23 @@ export interface SoundCompassProps {
63
74
  /** 스피커 버튼 크기(px). 기본 40 */
64
75
  buttonSize?: number;
65
76
  /** 스피커 버튼에 아이콘을 표시할지 여부. false면 숫자만 가운데 표시. 기본 true */
66
- showIcon?: boolean;
77
+ /**
78
+ * 버튼 안에 무엇을 그릴지.
79
+ *
80
+ * - `true` — 아이콘과 번호를 함께 (기본)
81
+ * - `false` — 번호만
82
+ * - `'answered'` — **답이 정해진 상태에서만 아이콘으로 갈아 끼운다.** 그때는
83
+ * 번호를 그리지 않는다. 나머지(`idle`·`active`)는 `false` 와 같이 번호만이다.
84
+ *
85
+ * `'answered'` 가 가르는 선은 자의적이지 않다. `idle` 과 `active` 는 아이콘이
86
+ * 둘 다 같은 `Volume2` 라 번호보다 더 말해 주는 것이 없다. `played`·`correct` 의
87
+ * 체크와 `wrong` 의 가위표만이 번호가 못 하는 말을 한다 — 아이콘이 자리를
88
+ * 차지할 값을 하는 자리만 바꿔 끼운다.
89
+ *
90
+ * 스피커를 번호로 부르는 화면에서 필요하다. `true` 로 켜면 고르지 않은 스피커까지
91
+ * 아이콘이 붙고 번호가 함께 작아져(실측: 36px → 18px) 번호로 부르는 화면이 무너진다.
92
+ */
93
+ showIcon?: boolean | 'answered';
67
94
  /**
68
95
  * `speakers[].shortcut` 배지가 버튼의 어느 모서리에 붙을지. 기본 우상단.
69
96
  *
@@ -91,4 +118,4 @@ export interface SoundCompassProps {
91
118
  scaleShortcut?: boolean;
92
119
  }
93
120
  /** @public */
94
- export default function SoundCompass({ speakers, size, viewT, activeSpeakerId, playedIds, selectedId, answerId, showFeedback, pulseSelected, pulseSelectedDuration, clickable, onSelectSpeaker, buttonSize, showIcon, shortcutAnchor, shortcutScale, scaleShortcut, }: SoundCompassProps): import("react").JSX.Element;
121
+ export default function SoundCompass({ speakers, size, viewT, activeSpeakerId, playedIds, selectedId, answerId, showFeedback, pulseSelected, pulseSelectedDuration, pulseScale, clickable, onSelectSpeaker, buttonSize, showIcon, shortcutAnchor, shortcutScale, scaleShortcut, }: SoundCompassProps): import("react").JSX.Element;
@@ -14,7 +14,7 @@ import ShortcutBadge from './ShortcutBadge';
14
14
  import './SoundCompass.css';
15
15
  const lerp = (a, b, t) => a + (b - a) * t;
16
16
  /** @public */
17
- export default function SoundCompass({ speakers, size = 300, viewT = 0, activeSpeakerId = null, playedIds, selectedId = null, answerId = null, showFeedback = false, pulseSelected = false, pulseSelectedDuration, clickable = false, onSelectSpeaker, buttonSize = 40, showIcon = true, shortcutAnchor = 'top-right', shortcutScale, scaleShortcut = false, }) {
17
+ export default function SoundCompass({ speakers, size = 300, viewT = 0, activeSpeakerId = null, playedIds, selectedId = null, answerId = null, showFeedback = false, pulseSelected = false, pulseSelectedDuration, pulseScale, clickable = false, onSelectSpeaker, buttonSize = 40, showIcon = true, shortcutAnchor = 'top-right', shortcutScale, scaleShortcut = false, }) {
18
18
  const t = Math.max(0, Math.min(1, viewT));
19
19
  const CENTER = size / 2;
20
20
  // ── 탑뷰 좌표 ──────────────────────────────────────────────────────────
@@ -192,12 +192,22 @@ export default function SoundCompass({ speakers, size = 300, viewT = 0, activeSp
192
192
  const iconSize = Math.round(Math.max(10, Math.min(56, BTN * 0.38)));
193
193
  const numberSize = Math.round(Math.max(7, Math.min(18, BTN * 0.2)));
194
194
  const numberOnlySize = Math.round(Math.max(10, Math.min(56, BTN * 0.4)));
195
- return (_jsxs("div", { className: "sound-compass__speaker-pos", style: { ...baseStyle, left: x - BTN / 2, top: y - BTN / 2 }, children: [pulse && (_jsx("div", { className: `sound-compass__pulse${pulse === 'once' ? ' sound-compass__pulse--once' : ''}`, style: {
195
+ // `'answered'` 는 답이 정해진 자리에서만 아이콘으로 **갈아 끼운다** —
196
+ // 아이콘과 번호를 같이 그리면 90px 버튼에서 둘 다 작아진다.
197
+ const swapped = showIcon === 'answered'
198
+ && (state === 'played' || state === 'correct' || state === 'wrong');
199
+ const withIcon = showIcon === true || swapped;
200
+ const withNumber = !swapped;
201
+ return (_jsxs("div", { className: "sound-compass__speaker-pos", style: { ...baseStyle, left: x - BTN / 2, top: y - BTN / 2 }, children: [pulse && (_jsx("div", { className: `sound-compass__pulse sound-compass__pulse--${state}`
202
+ + (pulse === 'once' ? ' sound-compass__pulse--once' : ''), style: {
196
203
  width: BTN, height: BTN,
197
- // 안 주면 아예 얹지 않는다 — CSS 의 기본 1s 가 살아 있게.
204
+ // 안 주면 아예 얹지 않는다 — CSS 의 기본값이 살아 있게.
198
205
  ...(pulse === 'once' && pulseSelectedDuration !== undefined
199
206
  ? { ['--sc-pulse-duration-once']: `${pulseSelectedDuration}ms` }
200
207
  : null),
201
- } })), _jsxs("button", { type: "button", tabIndex: clickable ? 0 : -1, "aria-disabled": clickable ? undefined : true, onClick: clickable ? () => onSelectSpeaker?.(sp.id) : undefined, className: `sound-compass__speaker-btn sound-compass__speaker-btn--${state}`, style: { width: BTN, height: BTN }, "aria-label": sp.label, children: [showIcon && getStateIcon(state, iconSize), _jsx("span", { className: "sound-compass__speaker-number", style: { fontSize: showIcon ? numberSize : numberOnlySize }, children: idx + 1 })] }), sp.shortcut && (_jsx(ShortcutBadge, { keys: sp.shortcut, anchor: shortcutAnchor, className: "sound-compass__speaker-shortcut" }))] }, sp.id));
208
+ ...(pulseScale !== undefined
209
+ ? { ['--sc-pulse-scale']: String(pulseScale) }
210
+ : null),
211
+ } })), _jsxs("button", { type: "button", tabIndex: clickable ? 0 : -1, "aria-disabled": clickable ? undefined : true, onClick: clickable ? () => onSelectSpeaker?.(sp.id) : undefined, className: `sound-compass__speaker-btn sound-compass__speaker-btn--${state}`, style: { width: BTN, height: BTN }, "aria-label": sp.label, children: [withIcon && getStateIcon(state, iconSize), withNumber && (_jsx("span", { className: "sound-compass__speaker-number", style: { fontSize: showIcon === true ? numberSize : numberOnlySize }, children: idx + 1 }))] }), sp.shortcut && (_jsx(ShortcutBadge, { keys: sp.shortcut, anchor: shortcutAnchor, className: "sound-compass__speaker-shortcut" }))] }, sp.id));
202
212
  })] }) }));
203
213
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ihabdevteam/core",
3
- "version": "0.19.0",
3
+ "version": "0.21.0",
4
4
  "private": false,
5
5
  "main": "dist/ihabdevteam-core.cjs.js",
6
6
  "module": "dist/ihabdevteam-core.esm.js",