@ihabdevteam/core 0.17.0 → 0.18.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, clickable, onSelectSpeaker, buttonSize, showIcon, shortcutAnchor, shortcutScale, scaleShortcut, }: SoundCompassProps): JSX.Element;
5122
+ export declare function SoundCompass({ speakers, size, viewT, activeSpeakerId, playedIds, selectedId, answerId, showFeedback, pulseSelected, clickable, onSelectSpeaker, buttonSize, showIcon, shortcutAnchor, shortcutScale, scaleShortcut, }: SoundCompassProps): JSX.Element;
5123
5123
 
5124
5124
  export declare interface SoundCompassExam {
5125
5125
  /** `<SoundCompass {...bind} />` 로 펼쳐 넣는 제어 props */
@@ -5186,6 +5186,20 @@ export declare interface SoundCompassProps {
5186
5186
  answerId?: string | null;
5187
5187
  /** 정답/오답을 시각적으로 표시할지 여부. 기본 false */
5188
5188
  showFeedback?: boolean;
5189
+ /**
5190
+ * 고른 스피커(`selectedId`)에도 울림을 붙일지. 기본 꺼짐.
5191
+ *
5192
+ * 울림은 원래 **소리가 나는 스피커**(`activeSpeakerId`)와 **피드백의 정답**에만
5193
+ * 붙는다. 정답을 알려주지 않는 검사에서는 그 둘을 쓰지 않으므로 울림이 한 번도
5194
+ * 안 난다 — 고른 자리에 손맛이 없다.
5195
+ *
5196
+ * 피드백을 보이는 동안에는 걸리지 않는다. 그때는 정답이 이미 울리고 있어서,
5197
+ * 고른 것까지 울리면 무엇을 가리키는 울림인지 흐려진다.
5198
+ *
5199
+ * 이 울림은 **한 번만** 번진다. 소리가 나는 동안 계속 알리는 것과 달리, 선택은
5200
+ * 일어난 순간을 알리는 것이라 되풀이할 까닭이 없다.
5201
+ */
5202
+ pulseSelected?: boolean;
5189
5203
  /** 스피커 클릭 가능 여부. 기본 false */
5190
5204
  clickable?: boolean;
5191
5205
  /** 스피커 선택 시 호출 */
@@ -136,6 +136,11 @@
136
136
  pointer-events: none;
137
137
  animation: sound-compass-pulse 1s cubic-bezier(0.33, 1, 0.68, 1) infinite;
138
138
  }
139
+ /* 고른 순간을 알리는 울림은 한 번만 번진다. 마지막 프레임이 opacity 0 이라
140
+ 되풀이를 끊는 것만으로 그대로 사라진다. */
141
+ .sound-compass__pulse--once {
142
+ animation-iteration-count: 1;
143
+ }
139
144
 
140
145
  /* 스피커 버튼 wrap의 정적 속성만 분리(나머지는 상태·좌표 기반 동적 값이라 인라인 유지) */
141
146
  /* 호버·포커스 — `all: unset` 이 브라우저 기본 상태까지 지워, 누를 수 있는 자리인지
@@ -30,6 +30,20 @@ export interface SoundCompassProps {
30
30
  answerId?: string | null;
31
31
  /** 정답/오답을 시각적으로 표시할지 여부. 기본 false */
32
32
  showFeedback?: boolean;
33
+ /**
34
+ * 고른 스피커(`selectedId`)에도 울림을 붙일지. 기본 꺼짐.
35
+ *
36
+ * 울림은 원래 **소리가 나는 스피커**(`activeSpeakerId`)와 **피드백의 정답**에만
37
+ * 붙는다. 정답을 알려주지 않는 검사에서는 그 둘을 쓰지 않으므로 울림이 한 번도
38
+ * 안 난다 — 고른 자리에 손맛이 없다.
39
+ *
40
+ * 피드백을 보이는 동안에는 걸리지 않는다. 그때는 정답이 이미 울리고 있어서,
41
+ * 고른 것까지 울리면 무엇을 가리키는 울림인지 흐려진다.
42
+ *
43
+ * 이 울림은 **한 번만** 번진다. 소리가 나는 동안 계속 알리는 것과 달리, 선택은
44
+ * 일어난 순간을 알리는 것이라 되풀이할 까닭이 없다.
45
+ */
46
+ pulseSelected?: boolean;
33
47
  /** 스피커 클릭 가능 여부. 기본 false */
34
48
  clickable?: boolean;
35
49
  /** 스피커 선택 시 호출 */
@@ -65,4 +79,4 @@ export interface SoundCompassProps {
65
79
  scaleShortcut?: boolean;
66
80
  }
67
81
  /** @public */
68
- export default function SoundCompass({ speakers, size, viewT, activeSpeakerId, playedIds, selectedId, answerId, showFeedback, clickable, onSelectSpeaker, buttonSize, showIcon, shortcutAnchor, shortcutScale, scaleShortcut, }: SoundCompassProps): import("react").JSX.Element;
82
+ export default function SoundCompass({ speakers, size, viewT, activeSpeakerId, playedIds, selectedId, answerId, showFeedback, pulseSelected, 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, 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, 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
  // ── 탑뷰 좌표 ──────────────────────────────────────────────────────────
@@ -132,8 +132,17 @@ export default function SoundCompass({ speakers, size = 300, viewT = 0, activeSp
132
132
  return 'played';
133
133
  return 'idle';
134
134
  };
135
- const shouldPulse = (sp) => activeSpeakerId === sp.id ||
136
- (showFeedback && sp.id === answerId);
135
+ /**
136
+ * 울림을 어떤 까닭으로 다는지. 까닭에 따라 되풀이가 다르다 — 소리가 나는 동안은
137
+ * 계속(`loop`), 고른 순간은 한 번(`once`).
138
+ */
139
+ const pulseKind = (sp) => {
140
+ if (activeSpeakerId === sp.id)
141
+ return 'loop';
142
+ if (showFeedback)
143
+ return sp.id === answerId ? 'loop' : null;
144
+ return pulseSelected && sp.id === selectedId ? 'once' : null;
145
+ };
137
146
  // SpeakerState → 아이콘. 색상/배경은 Button에 기대지 않고 SoundCompass.css의
138
147
  // .sound-compass__speaker-btn--{state} 클래스가 전담해, 자유롭게 바꿀 수 있다.
139
148
  const getStateIcon = (state, iconSize) => {
@@ -150,7 +159,7 @@ export default function SoundCompass({ speakers, size = 300, viewT = 0, activeSp
150
159
  }), rowArcPaths.map(a => (_jsx("path", { d: a.d, fill: "none", stroke: "#dde3ec", strokeWidth: "1" }, a.key)))] }), _jsx("div", { className: "sound-compass__center", style: { width: centerSize, height: centerSize, left: personPos.x, top: personPos.y }, children: _jsx(User, { size: centerIconSize, color: "currentColor", strokeWidth: 1.5 }) }), speakers.map((sp, idx) => {
151
160
  const { x, y } = getSpeakerXY(sp);
152
161
  const state = getSpeakerState(sp);
153
- const pulse = shouldPulse(sp);
162
+ const pulse = pulseKind(sp);
154
163
  const baseStyle = {
155
164
  // 배지는 24px 고정인데 버튼은 크게 오간다. 그대로 두면 버튼 대비 비율이
156
165
  // 세 배씩 흔들려, 작은 버튼에서는 배지가 버튼을 덮고 큰 버튼에서는
@@ -183,6 +192,6 @@ export default function SoundCompass({ speakers, size = 300, viewT = 0, activeSp
183
192
  const iconSize = Math.round(Math.max(10, Math.min(56, BTN * 0.38)));
184
193
  const numberSize = Math.round(Math.max(7, Math.min(18, BTN * 0.2)));
185
194
  const numberOnlySize = Math.round(Math.max(10, Math.min(56, BTN * 0.4)));
186
- 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", style: { width: BTN, height: BTN } }), _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));
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: { width: BTN, height: BTN } })), _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));
187
196
  })] }) }));
188
197
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ihabdevteam/core",
3
- "version": "0.17.0",
3
+ "version": "0.18.0",
4
4
  "private": false,
5
5
  "main": "dist/ihabdevteam-core.cjs.js",
6
6
  "module": "dist/ihabdevteam-core.esm.js",