@ssml-builder-js/ssml-editor-react 2.13.0 → 2.14.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ssml-builder-js/ssml-editor-react",
3
- "version": "2.13.0",
3
+ "version": "2.14.0",
4
4
  "description": "React-based SSML editor component",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -52,6 +52,6 @@
52
52
  }
53
53
  },
54
54
  "dependencies": {
55
- "@ssml-builder-js/ssml-core": "^2.13.0"
55
+ "@ssml-builder-js/ssml-core": "^2.14.0"
56
56
  }
57
57
  }
@@ -13,7 +13,10 @@ export interface VisualVoiceCatalogEntry {
13
13
  name: string;
14
14
  locale: string;
15
15
  region?: string;
16
+ regions?: readonly string[];
16
17
  styles?: readonly string[];
18
+ supportedTags?: readonly string[];
19
+ unsupportedTags?: readonly string[];
17
20
  preview?: boolean;
18
21
  status?: "ga" | "preview" | "deprecated";
19
22
  }
@@ -21,6 +24,7 @@ export interface VisualVoiceCatalogEntry {
21
24
  export interface VoiceSelectorRenderProps {
22
25
  value: string;
23
26
  voices: readonly VisualVoiceCatalogEntry[];
27
+ selectedVoice?: VisualVoiceCatalogEntry;
24
28
  readOnly: boolean;
25
29
  locale: SsmlEditorLocale;
26
30
  onChange: (voice: string) => void;
@@ -240,28 +244,82 @@ function filteredVoices(
240
244
  ): readonly VisualVoiceCatalogEntry[] {
241
245
  return (voiceCatalog ?? []).filter((voice) => {
242
246
  if (locale && voice.locale.toLowerCase() !== locale.toLowerCase()) return false;
243
- if (region && voice.region && voice.region.toLowerCase() !== region.toLowerCase()) return false;
247
+ const regions = [voice.region, ...(voice.regions ?? [])].filter((candidate): candidate is string =>
248
+ Boolean(candidate),
249
+ );
250
+ if (region && regions.length > 0 && !regions.some((candidate) => candidate.toLowerCase() === region.toLowerCase()))
251
+ return false;
244
252
  if (style && !voice.styles?.some((candidate) => candidate.toLowerCase() === style.toLowerCase())) return false;
245
253
  return true;
246
254
  });
247
255
  }
248
256
 
249
- function DefaultVoiceSelector({ value, voices, readOnly, locale, onChange }: VoiceSelectorRenderProps): ReactElement {
257
+ function VoiceCapabilityMatrix({
258
+ voice,
259
+ locale,
260
+ }: {
261
+ voice: VisualVoiceCatalogEntry;
262
+ locale: SsmlEditorLocale;
263
+ }): ReactElement {
264
+ const status = voice.status ?? (voice.preview ? "preview" : undefined);
250
265
  return (
251
- <select
252
- aria-label={locale === "ja" ? "音声" : "Voice"}
253
- value={value}
254
- disabled={readOnly}
255
- onChange={(event) => onChange(event.target.value)}
256
- >
257
- <option value="">{locale === "ja" ? "音声を選択" : "Select a voice"}</option>
258
- {voices.map((voice) => (
259
- <option key={voice.name} value={voice.name}>
260
- {voice.name}
261
- {voice.preview || voice.status === "preview" ? " (preview)" : ""}
262
- </option>
263
- ))}
264
- </select>
266
+ <div className="ssml-editor-voice-capabilities" data-voice-capabilities="">
267
+ <div>
268
+ {locale === "ja" ? "ステータス" : "Status"}: {status?.toUpperCase() ?? (locale === "ja" ? "GA" : "GA")}
269
+ {status === "preview" || status === "deprecated" ? (
270
+ <span role="status" aria-label={status} className="ssml-editor-voice-warning-badge">
271
+ {status === "preview" ? "⚠ Preview" : "⚠ Deprecated"}
272
+ </span>
273
+ ) : null}
274
+ </div>
275
+ {(voice.regions?.length || voice.region) && (
276
+ <div>
277
+ {locale === "ja" ? "リージョン" : "Regions"}:{" "}
278
+ {[...(voice.regions ?? []), ...(voice.region ? [voice.region] : [])]
279
+ .filter((region, index, all) => all.indexOf(region) === index)
280
+ .join(", ")}
281
+ </div>
282
+ )}
283
+ {voice.supportedTags && voice.supportedTags.length > 0 && (
284
+ <div>
285
+ {locale === "ja" ? "対応タグ" : "Supported tags"}: {voice.supportedTags.join(", ")}
286
+ </div>
287
+ )}
288
+ {voice.unsupportedTags && voice.unsupportedTags.length > 0 && (
289
+ <div className="ssml-editor-voice-unsupported">
290
+ {locale === "ja" ? "非対応タグ" : "Unsupported tags"}: {voice.unsupportedTags.join(", ")}
291
+ </div>
292
+ )}
293
+ </div>
294
+ );
295
+ }
296
+
297
+ function DefaultVoiceSelector({
298
+ value,
299
+ voices,
300
+ selectedVoice,
301
+ readOnly,
302
+ locale,
303
+ onChange,
304
+ }: VoiceSelectorRenderProps): ReactElement {
305
+ return (
306
+ <>
307
+ <select
308
+ aria-label={locale === "ja" ? "音声" : "Voice"}
309
+ value={value}
310
+ disabled={readOnly}
311
+ onChange={(event) => onChange(event.target.value)}
312
+ >
313
+ <option value="">{locale === "ja" ? "音声を選択" : "Select a voice"}</option>
314
+ {voices.map((voice) => (
315
+ <option key={voice.name} value={voice.name}>
316
+ {voice.name}
317
+ {voice.preview || voice.status === "preview" ? " (preview)" : ""}
318
+ </option>
319
+ ))}
320
+ </select>
321
+ {selectedVoice ? <VoiceCapabilityMatrix voice={selectedVoice} locale={locale} /> : null}
322
+ </>
265
323
  );
266
324
  }
267
325
 
@@ -297,6 +355,9 @@ function VisualElementInspector({
297
355
  }
298
356
  const fields = getElementFields(element);
299
357
  const availableVoices = filteredVoices(voiceCatalog, voiceLocale, voiceRegion, voiceStyle);
358
+ const selectedVoice = voiceCatalog?.find(
359
+ (voice) => voice.name === (element.type === "voice" ? element.name : undefined),
360
+ );
300
361
  const renderSelector = renderVoiceSelector ?? DefaultVoiceSelector;
301
362
  return (
302
363
  <fieldset>
@@ -352,6 +413,7 @@ function VisualElementInspector({
352
413
  renderSelector({
353
414
  value: inputValue,
354
415
  voices: availableVoices,
416
+ selectedVoice,
355
417
  readOnly,
356
418
  locale,
357
419
  onChange: (value) => commit(updateOptionalElementProperty(document, path, field.key, value)),
@@ -577,6 +639,7 @@ export function VisualSsmlEditor({
577
639
  (renderVoiceSelector ?? DefaultVoiceSelector)({
578
640
  value: selectedElement.voice ?? "",
579
641
  voices: filteredVoices(voiceCatalog, voiceLocale, voiceRegion, voiceStyle),
642
+ selectedVoice: voiceCatalog?.find((voice) => voice.name === selectedElement.voice),
580
643
  readOnly,
581
644
  locale,
582
645
  onChange: (value) =>