@ihabdevteam/core 0.16.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, }: 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
  /** 스피커 선택 시 호출 */
@@ -5208,6 +5222,17 @@ export declare interface SoundCompassProps {
5208
5222
  * 작아지고, 상한을 올려도 다음 소비자가 또 그 끝에서 막힌다. 그럴 때 직접 준다.
5209
5223
  */
5210
5224
  shortcutScale?: number;
5225
+ /**
5226
+ * 배지를 버튼 크기에 맞춰 스스로 키울지. 기본 꺼짐 — 배지는 24px 그대로다.
5227
+ *
5228
+ * 켜면 `clamp(0.75, buttonSize / 56, 3)` 이 걸린다(묶이지 않는 동안 버튼 대비
5229
+ * 0.43 으로 일정). `shortcutScale` 을 직접 주면 이것과 무관하게 그 값이 이긴다.
5230
+ *
5231
+ * 0.17.0 전에는 이 자동 배율이 **묻지 않고 늘 걸렸다.** 배지를 24px 그대로 두고
5232
+ * 싶은 소비자에게는 끌 방법이 없었고, 큰 버튼에서 배지가 갑자기 커지는 것이
5233
+ * 놀라움이 됐다. 스스로 정하는 쪽을 기본으로 두지 않는다.
5234
+ */
5235
+ scaleShortcut?: boolean;
5211
5236
  }
5212
5237
 
5213
5238
  /**
@@ -58,6 +58,8 @@
58
58
  /* 호버·포커스도 상태 색과 같은 방식으로 연다 — 소비자가 바꿔 끼울 수 있게. */
59
59
  --scb-idle-hover-bg: var(--color-whiteLine);
60
60
  --scb-idle-hover-border: var(--color-lightGray);
61
+ /* 호버의 글자색. 기본은 idle 과 같다 — 주지 않으면 예전 그대로다. */
62
+ --scb-idle-hover-fg: var(--scb-idle-fg);
61
63
  --scb-focus-ring: var(--color-main);
62
64
  }
63
65
 
@@ -134,6 +136,11 @@
134
136
  pointer-events: none;
135
137
  animation: sound-compass-pulse 1s cubic-bezier(0.33, 1, 0.68, 1) infinite;
136
138
  }
139
+ /* 고른 순간을 알리는 울림은 한 번만 번진다. 마지막 프레임이 opacity 0 이라
140
+ 되풀이를 끊는 것만으로 그대로 사라진다. */
141
+ .sound-compass__pulse--once {
142
+ animation-iteration-count: 1;
143
+ }
137
144
 
138
145
  /* 스피커 버튼 wrap의 정적 속성만 분리(나머지는 상태·좌표 기반 동적 값이라 인라인 유지) */
139
146
  /* 호버·포커스 — `all: unset` 이 브라우저 기본 상태까지 지워, 누를 수 있는 자리인지
@@ -149,6 +156,7 @@
149
156
  .sound-compass__speaker-btn--idle:hover {
150
157
  background: var(--scb-idle-hover-bg);
151
158
  border-color: var(--scb-idle-hover-border);
159
+ color: var(--scb-idle-hover-fg);
152
160
  }
153
161
  /* :focus-visible 이라 마우스로 누를 때는 링이 안 뜨고 키보드로 옮길 때만 뜬다. */
154
162
  .sound-compass__speaker-btn:focus-visible {
@@ -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
  /** 스피커 선택 시 호출 */
@@ -52,6 +66,17 @@ export interface SoundCompassProps {
52
66
  * 작아지고, 상한을 올려도 다음 소비자가 또 그 끝에서 막힌다. 그럴 때 직접 준다.
53
67
  */
54
68
  shortcutScale?: number;
69
+ /**
70
+ * 배지를 버튼 크기에 맞춰 스스로 키울지. 기본 꺼짐 — 배지는 24px 그대로다.
71
+ *
72
+ * 켜면 `clamp(0.75, buttonSize / 56, 3)` 이 걸린다(묶이지 않는 동안 버튼 대비
73
+ * 0.43 으로 일정). `shortcutScale` 을 직접 주면 이것과 무관하게 그 값이 이긴다.
74
+ *
75
+ * 0.17.0 전에는 이 자동 배율이 **묻지 않고 늘 걸렸다.** 배지를 24px 그대로 두고
76
+ * 싶은 소비자에게는 끌 방법이 없었고, 큰 버튼에서 배지가 갑자기 커지는 것이
77
+ * 놀라움이 됐다. 스스로 정하는 쪽을 기본으로 두지 않는다.
78
+ */
79
+ scaleShortcut?: boolean;
55
80
  }
56
81
  /** @public */
57
- export default function SoundCompass({ speakers, size, viewT, activeSpeakerId, playedIds, selectedId, answerId, showFeedback, clickable, onSelectSpeaker, buttonSize, showIcon, shortcutAnchor, shortcutScale, }: 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, }) {
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
  // 세 배씩 흔들려, 작은 버튼에서는 배지가 버튼을 덮고 큰 버튼에서는
@@ -162,12 +171,16 @@ export default function SoundCompass({ speakers, size = 300, viewT = 0, activeSp
162
171
  // 3 이면 168px 까지 묶이지 않는다. 하한 0.75 는 배지가 읽히는 크기다.
163
172
  //
164
173
  // 그래도 이 식이 안 맞는 자리는 남는다 — 상한을 올려도 다음 소비자가 또
165
- // 그 끝에서 막힌다. 그래서 `shortcutScale` 로 직접 줄 수 있게 열어 두고,
166
- // 안 주면 이 식이 그대로 쓰인다.
174
+ // 그 끝에서 막힌다. 그래서 `shortcutScale` 로 직접 줄 수 있게 열어 둔다.
175
+ //
176
+ // **둘 다 안 주면 변수를 아예 얹지 않는다.** 그러면 CSS 의 기본값 1 이
177
+ // 살아 배지가 24px 그대로다. 예전에는 이 식이 묻지 않고 늘 걸렸다.
167
178
  //
168
179
  // 배율은 transform 이 아니라 변수로 넘긴다 — 배지의 anchor 가 같은 속성에
169
180
  // translate 를 걸고 있어, 여기서 transform 을 걸면 그것을 지운다.
170
- ['--shortcut-badge-scale']: String(shortcutScale ?? Math.max(0.75, Math.min(3, buttonSize / 56))),
181
+ ...(shortcutScale !== undefined || scaleShortcut
182
+ ? { ['--shortcut-badge-scale']: String(shortcutScale ?? Math.max(0.75, Math.min(3, buttonSize / 56))) }
183
+ : null),
171
184
  pointerEvents: (clickable ? 'auto' : 'none'),
172
185
  cursor: clickable ? 'pointer' : 'default',
173
186
  // 호출된(=idle이 아닌) 스피커를 다른 스피커 위로 끌어올렸다가, 초기화되면 다시 낮춘다
@@ -179,6 +192,6 @@ export default function SoundCompass({ speakers, size = 300, viewT = 0, activeSp
179
192
  const iconSize = Math.round(Math.max(10, Math.min(56, BTN * 0.38)));
180
193
  const numberSize = Math.round(Math.max(7, Math.min(18, BTN * 0.2)));
181
194
  const numberOnlySize = Math.round(Math.max(10, Math.min(56, BTN * 0.4)));
182
- 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));
183
196
  })] }) }));
184
197
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ihabdevteam/core",
3
- "version": "0.16.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",