@intellectif/lk-react 13.0.0 → 14.0.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 (162) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/README.md +11 -3
  3. package/dist/{WrittenResponse-B5p_-RnJ.d.ts → WrittenResponse-C0P34En1.d.ts} +1 -1
  4. package/dist/{WrittenResponse-DWtS4tOL.d.cts → WrittenResponse-pB1ik8hg.d.cts} +1 -1
  5. package/dist/chunk-36FTYJWJ.cjs +869 -0
  6. package/dist/chunk-36FTYJWJ.cjs.map +1 -0
  7. package/dist/{chunk-PSHNTDKG.js → chunk-47HHC4GD.js} +13 -161
  8. package/dist/chunk-47HHC4GD.js.map +1 -0
  9. package/dist/chunk-62AKSD5Y.js +509 -0
  10. package/dist/chunk-62AKSD5Y.js.map +1 -0
  11. package/dist/{chunk-XAGWBVK3.js → chunk-72UGRIMM.js} +6 -5
  12. package/dist/chunk-72UGRIMM.js.map +1 -0
  13. package/dist/{chunk-SBIJJ374.cjs → chunk-B2XQLU2E.cjs} +5 -5
  14. package/dist/{chunk-SBIJJ374.cjs.map → chunk-B2XQLU2E.cjs.map} +1 -1
  15. package/dist/{chunk-DQERNAWI.js → chunk-BO6P723E.js} +26 -31
  16. package/dist/chunk-BO6P723E.js.map +1 -0
  17. package/dist/chunk-BRPEB43C.cjs +13 -0
  18. package/dist/chunk-BRPEB43C.cjs.map +1 -0
  19. package/dist/{chunk-NVUFEMPZ.cjs → chunk-BVRUUYYR.cjs} +30 -16
  20. package/dist/chunk-BVRUUYYR.cjs.map +1 -0
  21. package/dist/{chunk-AMGK7DDM.cjs → chunk-D4DQFTA7.cjs} +2 -2
  22. package/dist/chunk-D4DQFTA7.cjs.map +1 -0
  23. package/dist/chunk-D57QZILZ.js +863 -0
  24. package/dist/chunk-D57QZILZ.js.map +1 -0
  25. package/dist/chunk-F4TBCRGI.cjs +518 -0
  26. package/dist/chunk-F4TBCRGI.cjs.map +1 -0
  27. package/dist/{chunk-YILH4ETT.js → chunk-I33FOKZ7.js} +224 -31
  28. package/dist/chunk-I33FOKZ7.js.map +1 -0
  29. package/dist/{chunk-HT4FFZZQ.cjs → chunk-J2CDPIRC.cjs} +231 -38
  30. package/dist/chunk-J2CDPIRC.cjs.map +1 -0
  31. package/dist/{chunk-MA7GXUWC.cjs → chunk-LB3FEV7Z.cjs} +77 -2
  32. package/dist/chunk-LB3FEV7Z.cjs.map +1 -0
  33. package/dist/{chunk-5N7OLFHB.js → chunk-LGCUOLP6.js} +6 -5
  34. package/dist/chunk-LGCUOLP6.js.map +1 -0
  35. package/dist/{chunk-YZWXVZB3.js → chunk-LLXCX5KK.js} +6 -5
  36. package/dist/chunk-LLXCX5KK.js.map +1 -0
  37. package/dist/chunk-MCO52PV5.js +520 -0
  38. package/dist/chunk-MCO52PV5.js.map +1 -0
  39. package/dist/{chunk-K7NMNOPI.cjs → chunk-NDQD3ZRY.cjs} +15 -14
  40. package/dist/chunk-NDQD3ZRY.cjs.map +1 -0
  41. package/dist/chunk-OD65OZIW.cjs +159 -0
  42. package/dist/chunk-OD65OZIW.cjs.map +1 -0
  43. package/dist/chunk-RCMYEWBE.js +11 -0
  44. package/dist/chunk-RCMYEWBE.js.map +1 -0
  45. package/dist/{chunk-74QXBWCI.cjs → chunk-RRH7ZPXC.cjs} +6 -6
  46. package/dist/{chunk-74QXBWCI.cjs.map → chunk-RRH7ZPXC.cjs.map} +1 -1
  47. package/dist/{chunk-DQNVAXG6.js → chunk-RXS455MR.js} +2 -2
  48. package/dist/chunk-RXS455MR.js.map +1 -0
  49. package/dist/{chunk-Z63JRG4M.cjs → chunk-SE6WWDVW.cjs} +15 -14
  50. package/dist/chunk-SE6WWDVW.cjs.map +1 -0
  51. package/dist/{chunk-NVP6UEQQ.js → chunk-SWY6PWXK.js} +4 -4
  52. package/dist/{chunk-NVP6UEQQ.js.map → chunk-SWY6PWXK.js.map} +1 -1
  53. package/dist/{chunk-3Q4I4KT6.js → chunk-TDJEKXEP.js} +77 -2
  54. package/dist/chunk-TDJEKXEP.js.map +1 -0
  55. package/dist/{chunk-2GXSA322.cjs → chunk-U3APKQ7P.cjs} +15 -14
  56. package/dist/chunk-U3APKQ7P.cjs.map +1 -0
  57. package/dist/chunk-UDCQTOVU.cjs +523 -0
  58. package/dist/chunk-UDCQTOVU.cjs.map +1 -0
  59. package/dist/{chunk-K3EA547J.cjs → chunk-USBANZ7K.cjs} +27 -33
  60. package/dist/chunk-USBANZ7K.cjs.map +1 -0
  61. package/dist/{chunk-J73TDY7J.cjs → chunk-WTS34S5E.cjs} +25 -173
  62. package/dist/chunk-WTS34S5E.cjs.map +1 -0
  63. package/dist/{chunk-R7DI5UMU.js → chunk-XIBOFTOL.js} +3 -3
  64. package/dist/{chunk-R7DI5UMU.js.map → chunk-XIBOFTOL.js.map} +1 -1
  65. package/dist/{chunk-W4MXRZAB.js → chunk-XUEIUEPI.js} +6 -5
  66. package/dist/chunk-XUEIUEPI.js.map +1 -0
  67. package/dist/{chunk-UU2NV6SU.cjs → chunk-YEYKJFEC.cjs} +14 -13
  68. package/dist/chunk-YEYKJFEC.cjs.map +1 -0
  69. package/dist/{chunk-PY53ABWC.js → chunk-YMBBM5K7.js} +23 -9
  70. package/dist/chunk-YMBBM5K7.js.map +1 -0
  71. package/dist/chunk-YORQQXGO.js +155 -0
  72. package/dist/chunk-YORQQXGO.js.map +1 -0
  73. package/dist/components/ActivityPreview.cjs +15 -10
  74. package/dist/components/ActivityPreview.d.cts +9 -3
  75. package/dist/components/ActivityPreview.d.ts +9 -3
  76. package/dist/components/ActivityPreview.js +14 -9
  77. package/dist/components/ActivitySequence.cjs +16 -11
  78. package/dist/components/ActivitySequence.d.cts +108 -4
  79. package/dist/components/ActivitySequence.d.ts +108 -4
  80. package/dist/components/ActivitySequence.js +15 -10
  81. package/dist/components/Dictation.cjs +7 -5
  82. package/dist/components/Dictation.d.cts +2 -1
  83. package/dist/components/Dictation.d.ts +2 -1
  84. package/dist/components/Dictation.js +6 -4
  85. package/dist/components/FillInTheBlanks.cjs +6 -5
  86. package/dist/components/FillInTheBlanks.d.cts +2 -1
  87. package/dist/components/FillInTheBlanks.d.ts +2 -1
  88. package/dist/components/FillInTheBlanks.js +5 -4
  89. package/dist/components/GapSelect.cjs +6 -5
  90. package/dist/components/GapSelect.d.cts +2 -1
  91. package/dist/components/GapSelect.d.ts +2 -1
  92. package/dist/components/GapSelect.js +5 -4
  93. package/dist/components/MultipleChoice.cjs +6 -5
  94. package/dist/components/MultipleChoice.d.cts +2 -1
  95. package/dist/components/MultipleChoice.d.ts +2 -1
  96. package/dist/components/MultipleChoice.js +5 -4
  97. package/dist/components/PronunciationFeedback.cjs +16 -0
  98. package/dist/components/PronunciationFeedback.cjs.map +1 -0
  99. package/dist/components/PronunciationFeedback.d.cts +64 -0
  100. package/dist/components/PronunciationFeedback.d.ts +64 -0
  101. package/dist/components/PronunciationFeedback.js +7 -0
  102. package/dist/components/PronunciationFeedback.js.map +1 -0
  103. package/dist/components/ReadAloud.cjs +21 -0
  104. package/dist/components/ReadAloud.cjs.map +1 -0
  105. package/dist/components/ReadAloud.d.cts +15 -0
  106. package/dist/components/ReadAloud.d.ts +15 -0
  107. package/dist/components/ReadAloud.js +12 -0
  108. package/dist/components/ReadAloud.js.map +1 -0
  109. package/dist/components/StimulusPanel.cjs +4 -4
  110. package/dist/components/StimulusPanel.d.cts +2 -1
  111. package/dist/components/StimulusPanel.d.ts +2 -1
  112. package/dist/components/StimulusPanel.js +3 -3
  113. package/dist/components/WrittenResponse.cjs +6 -5
  114. package/dist/components/WrittenResponse.d.cts +4 -3
  115. package/dist/components/WrittenResponse.d.ts +4 -3
  116. package/dist/components/WrittenResponse.js +5 -4
  117. package/dist/hooks/useSpeechRecorder.cjs +17 -0
  118. package/dist/hooks/useSpeechRecorder.cjs.map +1 -0
  119. package/dist/hooks/useSpeechRecorder.d.cts +127 -0
  120. package/dist/hooks/useSpeechRecorder.d.ts +127 -0
  121. package/dist/hooks/useSpeechRecorder.js +4 -0
  122. package/dist/hooks/useSpeechRecorder.js.map +1 -0
  123. package/dist/i18n/LkIntlProvider.cjs +7 -7
  124. package/dist/i18n/LkIntlProvider.d.cts +3 -2
  125. package/dist/i18n/LkIntlProvider.d.ts +3 -2
  126. package/dist/i18n/LkIntlProvider.js +1 -1
  127. package/dist/index.cjs +53 -32
  128. package/dist/index.cjs.map +1 -1
  129. package/dist/index.d.cts +6 -2
  130. package/dist/index.d.ts +6 -2
  131. package/dist/index.js +17 -12
  132. package/dist/index.js.map +1 -1
  133. package/dist/machine-CuwKqWXC.d.cts +26 -0
  134. package/dist/machine-CuwKqWXC.d.ts +26 -0
  135. package/dist/{strings-DPTyLpHt.d.ts → strings--ADnHcaA.d.cts} +214 -4
  136. package/dist/{strings-DPTyLpHt.d.cts → strings-CtdSf2St.d.ts} +214 -4
  137. package/dist/theme/ThemeProvider.cjs +6 -6
  138. package/dist/theme/ThemeProvider.d.cts +4 -4
  139. package/dist/theme/ThemeProvider.d.ts +4 -4
  140. package/dist/theme/ThemeProvider.js +1 -1
  141. package/dist/theme/skin.css +596 -3
  142. package/package.json +33 -3
  143. package/dist/chunk-2GXSA322.cjs.map +0 -1
  144. package/dist/chunk-3Q4I4KT6.js.map +0 -1
  145. package/dist/chunk-5N7OLFHB.js.map +0 -1
  146. package/dist/chunk-AMGK7DDM.cjs.map +0 -1
  147. package/dist/chunk-DQERNAWI.js.map +0 -1
  148. package/dist/chunk-DQNVAXG6.js.map +0 -1
  149. package/dist/chunk-HT4FFZZQ.cjs.map +0 -1
  150. package/dist/chunk-J73TDY7J.cjs.map +0 -1
  151. package/dist/chunk-K3EA547J.cjs.map +0 -1
  152. package/dist/chunk-K7NMNOPI.cjs.map +0 -1
  153. package/dist/chunk-MA7GXUWC.cjs.map +0 -1
  154. package/dist/chunk-NVUFEMPZ.cjs.map +0 -1
  155. package/dist/chunk-PSHNTDKG.js.map +0 -1
  156. package/dist/chunk-PY53ABWC.js.map +0 -1
  157. package/dist/chunk-UU2NV6SU.cjs.map +0 -1
  158. package/dist/chunk-W4MXRZAB.js.map +0 -1
  159. package/dist/chunk-XAGWBVK3.js.map +0 -1
  160. package/dist/chunk-YILH4ETT.js.map +0 -1
  161. package/dist/chunk-YZWXVZB3.js.map +0 -1
  162. package/dist/chunk-Z63JRG4M.cjs.map +0 -1
@@ -1,7 +1,7 @@
1
1
  /*
2
- * learning-kit OPTIONAL default skin — a slick, neutral monochrome look
3
- * (ElevenLabs-like). Entirely token-driven, so `ThemeProvider` / dark mode
4
- * restyle it for free.
2
+ * learning-kit OPTIONAL default skin — a neutral monochrome, deliberately
3
+ * unbranded. Entirely token-driven, so `ThemeProvider` / dark mode restyle it
4
+ * for free.
5
5
  *
6
6
  * Override contract: every rule lives in the `lk-skin` CSS cascade layer.
7
7
  * Unlayered author styles ALWAYS win over layered styles, so a consumer can
@@ -1326,3 +1326,596 @@
1326
1326
  }
1327
1327
  }
1328
1328
  }
1329
+
1330
+ /*
1331
+ * Read aloud, and the pronunciation feedback panel it renders.
1332
+ *
1333
+ * The marking vocabulary is the dictation's, on purpose: the same glyphs
1334
+ * (✓ ✗ ∅ +), the same four decorations and the same four tokens, with
1335
+ * `mispronounced` standing exactly where `incorrect` stands there. A learner
1336
+ * who meets both activity types reads one set of marks rather than two,
1337
+ * translators get no new concepts, and no token is invented — only the class
1338
+ * prefixes differ, so the two can still be restyled apart.
1339
+ *
1340
+ * `.lk-pf-*` is the feedback panel. It renders INSIDE the form in practice and
1341
+ * stands alone in a report or a teacher's view, so it carries its own box and
1342
+ * gives that box up only when it is nested.
1343
+ *
1344
+ * Logical properties throughout: the reading may be in a right-to-left
1345
+ * language inside a left-to-right interface, and the marks follow the reading.
1346
+ */
1347
+ @layer lk-skin {
1348
+ .lk-ra,
1349
+ .lk-pf {
1350
+ display: flex;
1351
+ flex-direction: column;
1352
+ gap: var(--lk-spacing-md);
1353
+ font-family: var(--lk-font-family-base);
1354
+ font-size: var(--lk-font-size-base);
1355
+ line-height: var(--lk-line-height-base);
1356
+ color: var(--lk-color-text);
1357
+ background: var(--lk-color-surface);
1358
+ padding: var(--lk-spacing-lg);
1359
+ border: 1px solid var(--lk-color-border);
1360
+ border-radius: var(--lk-radius-lg);
1361
+ }
1362
+
1363
+ .lk-ra *,
1364
+ .lk-pf * {
1365
+ box-sizing: border-box;
1366
+ }
1367
+
1368
+ /* Nested inside the activity it explains: one box, not a box in a box. */
1369
+ .lk-ra .lk-pf {
1370
+ padding: 0;
1371
+ border: 0;
1372
+ border-radius: 0;
1373
+ background: none;
1374
+ }
1375
+
1376
+ .lk-ra fieldset {
1377
+ margin: 0;
1378
+ padding: 0;
1379
+ border: 0;
1380
+ min-inline-size: 0;
1381
+ }
1382
+
1383
+ .lk-ra-title {
1384
+ margin: 0;
1385
+ font-size: var(--lk-font-size-lg);
1386
+ font-weight: var(--lk-font-weight-bold);
1387
+ }
1388
+
1389
+ .lk-ra-instructions {
1390
+ margin: 0;
1391
+ font-size: var(--lk-font-size-sm);
1392
+ color: var(--lk-color-text-muted);
1393
+ }
1394
+
1395
+ /* The words to read ARE the activity: set apart from the chrome around them,
1396
+ and large enough to read from a phone held at arm's length. */
1397
+ .lk-ra-reference {
1398
+ margin: 0;
1399
+ padding: var(--lk-spacing-sm) var(--lk-spacing-md);
1400
+ border-inline-start: 3px solid var(--lk-color-primary);
1401
+ border-radius: var(--lk-radius-sm);
1402
+ background: var(--lk-color-surface-raised);
1403
+ font-size: var(--lk-font-size-lg);
1404
+ }
1405
+
1406
+ /* ── The model recordings ──────────────────────────────────────────── */
1407
+ .lk-ra-models {
1408
+ display: flex;
1409
+ flex-direction: column;
1410
+ gap: var(--lk-spacing-sm);
1411
+ }
1412
+
1413
+ .lk-ra-model-label {
1414
+ padding: 0;
1415
+ margin-block-end: var(--lk-spacing-xs);
1416
+ font-size: var(--lk-font-size-sm);
1417
+ font-weight: var(--lk-font-weight-bold);
1418
+ color: var(--lk-color-text-muted);
1419
+ }
1420
+
1421
+ .lk-ra-model .lk-media {
1422
+ margin: 0;
1423
+ }
1424
+
1425
+ /* ── The recorder ──────────────────────────────────────────────────── */
1426
+ .lk-ra-recorder,
1427
+ .lk-ra-actions {
1428
+ display: flex;
1429
+ flex-wrap: wrap;
1430
+ align-items: center;
1431
+ gap: var(--lk-spacing-sm);
1432
+ }
1433
+
1434
+ .lk-ra-record,
1435
+ .lk-ra-stop,
1436
+ .lk-ra-submit-blank,
1437
+ .lk-ra-retry,
1438
+ .lk-pf-word-toggle,
1439
+ .lk-pf-word-play {
1440
+ padding: var(--lk-spacing-xs) var(--lk-spacing-md);
1441
+ font: inherit;
1442
+ font-size: var(--lk-font-size-sm);
1443
+ color: var(--lk-color-text);
1444
+ background: var(--lk-color-surface);
1445
+ border: 1px solid var(--lk-color-border);
1446
+ border-radius: var(--lk-radius-base);
1447
+ cursor: pointer;
1448
+ transition: var(--lk-transition-fast);
1449
+ }
1450
+
1451
+ .lk-ra-record:hover:not(:disabled):not([aria-disabled="true"]),
1452
+ .lk-ra-stop:hover:not(:disabled),
1453
+ .lk-ra-submit-blank:hover:not(:disabled):not([aria-disabled="true"]),
1454
+ .lk-ra-retry:hover:not(:disabled),
1455
+ .lk-pf-word-toggle:hover,
1456
+ .lk-pf-word-play:hover {
1457
+ border-color: var(--lk-color-primary);
1458
+ }
1459
+
1460
+ /* The take budget is spent, or a submit is in flight: aria-disabled rather
1461
+ than disabled, so the control a learner comes back to keeps focus. */
1462
+ .lk-ra-record:disabled,
1463
+ .lk-ra-record[aria-disabled="true"],
1464
+ .lk-ra-stop:disabled,
1465
+ .lk-ra-submit:disabled,
1466
+ .lk-ra-submit[aria-disabled="true"],
1467
+ .lk-ra-submit-blank:disabled,
1468
+ .lk-ra-submit-blank[aria-disabled="true"] {
1469
+ opacity: 0.55;
1470
+ cursor: default;
1471
+ }
1472
+
1473
+ /* Recording is the one state a learner must read at a glance, and the stop
1474
+ control is the only way out of it. */
1475
+ .lk-ra-stop {
1476
+ border-color: var(--lk-color-error);
1477
+ color: var(--lk-color-error);
1478
+ font-weight: var(--lk-font-weight-bold);
1479
+ }
1480
+
1481
+ .lk-ra-submit {
1482
+ padding: var(--lk-spacing-sm) var(--lk-spacing-lg);
1483
+ font: inherit;
1484
+ font-weight: var(--lk-font-weight-bold);
1485
+ color: var(--lk-color-surface);
1486
+ background: var(--lk-color-primary);
1487
+ border: 0;
1488
+ border-radius: var(--lk-radius-base);
1489
+ cursor: pointer;
1490
+ transition: var(--lk-transition-fast);
1491
+ }
1492
+
1493
+ .lk-ra-submit:hover:not(:disabled):not([aria-disabled="true"]) {
1494
+ background: var(--lk-color-primary-hover);
1495
+ }
1496
+
1497
+ .lk-ra-progress,
1498
+ .lk-ra-takes,
1499
+ .lk-ra-pending {
1500
+ flex: 1 0 100%;
1501
+ margin: 0;
1502
+ font-size: var(--lk-font-size-sm);
1503
+ color: var(--lk-color-text-muted);
1504
+ }
1505
+
1506
+ /*
1507
+ * The input meter, in the ten steps `data-level` carries. One rule per step,
1508
+ * because CSS cannot read a number out of an attribute and use it as a
1509
+ * length. It is decoration and nothing else — the recorder's state is
1510
+ * already in the button's own words and in `data-status`, and the start and
1511
+ * the end of a take are announced by the component — so a page that
1512
+ * overrides it away loses nothing a learner needs.
1513
+ */
1514
+ .lk-ra-recorder[data-level]::after {
1515
+ content: "";
1516
+ flex: 1 0 100%;
1517
+ block-size: 4px;
1518
+ inline-size: 0;
1519
+ border-radius: var(--lk-radius-sm);
1520
+ background: var(--lk-color-primary);
1521
+ transition: var(--lk-transition-fast);
1522
+ }
1523
+
1524
+ .lk-ra-recorder[data-level="1"]::after {
1525
+ inline-size: 10%;
1526
+ }
1527
+
1528
+ .lk-ra-recorder[data-level="2"]::after {
1529
+ inline-size: 20%;
1530
+ }
1531
+
1532
+ .lk-ra-recorder[data-level="3"]::after {
1533
+ inline-size: 30%;
1534
+ }
1535
+
1536
+ .lk-ra-recorder[data-level="4"]::after {
1537
+ inline-size: 40%;
1538
+ }
1539
+
1540
+ .lk-ra-recorder[data-level="5"]::after {
1541
+ inline-size: 50%;
1542
+ }
1543
+
1544
+ .lk-ra-recorder[data-level="6"]::after {
1545
+ inline-size: 60%;
1546
+ }
1547
+
1548
+ .lk-ra-recorder[data-level="7"]::after {
1549
+ inline-size: 70%;
1550
+ }
1551
+
1552
+ .lk-ra-recorder[data-level="8"]::after {
1553
+ inline-size: 80%;
1554
+ }
1555
+
1556
+ .lk-ra-recorder[data-level="9"]::after {
1557
+ inline-size: 90%;
1558
+ }
1559
+
1560
+ .lk-ra-recorder[data-level="10"]::after {
1561
+ inline-size: 100%;
1562
+ }
1563
+
1564
+ /* The learner's own take, and the one a review plays back. */
1565
+ .lk-ra-take {
1566
+ display: block;
1567
+ inline-size: 100%;
1568
+ }
1569
+
1570
+ /* ── What came back ────────────────────────────────────────────────── */
1571
+ .lk-ra-alert {
1572
+ margin: 0;
1573
+ padding: var(--lk-spacing-xs) var(--lk-spacing-sm);
1574
+ border-inline-start: 3px solid var(--lk-color-error);
1575
+ border-radius: var(--lk-radius-sm);
1576
+ background: var(--lk-color-surface-raised);
1577
+ color: var(--lk-color-error);
1578
+ }
1579
+
1580
+ .lk-ra-outcome,
1581
+ .lk-pf-grade {
1582
+ display: flex;
1583
+ flex-direction: column;
1584
+ gap: var(--lk-spacing-xs);
1585
+ }
1586
+
1587
+ .lk-ra-grade,
1588
+ .lk-pf-score {
1589
+ margin: 0;
1590
+ font-size: var(--lk-font-size-lg);
1591
+ font-weight: var(--lk-font-weight-bold);
1592
+ }
1593
+
1594
+ .lk-ra-grade-feedback,
1595
+ .lk-pf-grade-feedback {
1596
+ margin: 0;
1597
+ color: var(--lk-color-text-muted);
1598
+ }
1599
+
1600
+ /* Passed or not is a word in the score sentence first; the colour is the
1601
+ second channel and never the only one. */
1602
+ .lk-ra-outcome[data-passed="true"] .lk-ra-grade,
1603
+ .lk-pf-grade[data-passed="true"] .lk-pf-score {
1604
+ color: var(--lk-color-success);
1605
+ }
1606
+
1607
+ .lk-ra-outcome[data-passed="false"] .lk-ra-grade,
1608
+ .lk-pf-grade[data-passed="false"] .lk-pf-score {
1609
+ color: var(--lk-color-error);
1610
+ }
1611
+
1612
+ /* ── The dimensions a take was graded on ───────────────────────────── */
1613
+ .lk-pf-dimensions {
1614
+ display: flex;
1615
+ flex-wrap: wrap;
1616
+ gap: var(--lk-spacing-sm);
1617
+ margin: 0;
1618
+ }
1619
+
1620
+ .lk-pf-dimension {
1621
+ flex: 1 1 auto;
1622
+ padding: var(--lk-spacing-sm) var(--lk-spacing-md);
1623
+ border: 1px solid var(--lk-color-border);
1624
+ border-radius: var(--lk-radius-base);
1625
+ background: var(--lk-color-surface-raised);
1626
+ }
1627
+
1628
+ .lk-pf-dimension-name {
1629
+ font-size: var(--lk-font-size-sm);
1630
+ color: var(--lk-color-text-muted);
1631
+ }
1632
+
1633
+ .lk-pf-dimension-score {
1634
+ margin: 0;
1635
+ font-size: var(--lk-font-size-lg);
1636
+ font-weight: var(--lk-font-weight-bold);
1637
+ }
1638
+
1639
+ /* ── The marks ─────────────────────────────────────────────────────── */
1640
+ .lk-ra-marks,
1641
+ .lk-pf-word-detail {
1642
+ display: flex;
1643
+ flex-direction: column;
1644
+ gap: var(--lk-spacing-sm);
1645
+ padding: var(--lk-spacing-md);
1646
+ border: 1px solid var(--lk-color-border);
1647
+ border-radius: var(--lk-radius-base);
1648
+ background: var(--lk-color-surface-raised);
1649
+ }
1650
+
1651
+ .lk-ra-words,
1652
+ .lk-ra-legend,
1653
+ .lk-pf-words,
1654
+ .lk-pf-legend,
1655
+ .lk-pf-syllables,
1656
+ .lk-pf-phonemes,
1657
+ .lk-pf-heard-as {
1658
+ display: flex;
1659
+ flex-wrap: wrap;
1660
+ gap: var(--lk-spacing-xs) var(--lk-spacing-sm);
1661
+ margin: 0;
1662
+ padding: 0;
1663
+ list-style: none;
1664
+ }
1665
+
1666
+ .lk-ra-word,
1667
+ .lk-pf-word {
1668
+ /* The hidden sentence is positioned against the word it describes. */
1669
+ position: relative;
1670
+ display: flex;
1671
+ flex-wrap: wrap;
1672
+ align-items: center;
1673
+ gap: var(--lk-spacing-xs);
1674
+ font-size: var(--lk-font-size-lg);
1675
+ }
1676
+
1677
+ /* An opened word's panel takes the whole row, under the word it belongs to. */
1678
+ .lk-pf-word-detail {
1679
+ flex: 1 0 100%;
1680
+ font-size: var(--lk-font-size-base);
1681
+ }
1682
+
1683
+ .lk-ra-word-text,
1684
+ .lk-pf-word-text {
1685
+ display: inline-block;
1686
+ padding: 0 var(--lk-spacing-xs);
1687
+ border-radius: var(--lk-radius-sm);
1688
+ }
1689
+
1690
+ .lk-ra-legend,
1691
+ .lk-pf-legend {
1692
+ font-size: var(--lk-font-size-sm);
1693
+ color: var(--lk-color-text-muted);
1694
+ }
1695
+
1696
+ .lk-ra-legend li,
1697
+ .lk-pf-legend li {
1698
+ padding: 0 var(--lk-spacing-xs);
1699
+ }
1700
+
1701
+ /* One glyph per state, declared decorative: the translatable announcement is
1702
+ the component's hidden sentence, not the stylesheet's. */
1703
+ .lk-ra-word-text::before,
1704
+ .lk-pf-word-text::before,
1705
+ .lk-ra-legend li[data-state]::before,
1706
+ .lk-pf-legend li[data-state]::before {
1707
+ margin-inline-end: 0.3em;
1708
+ font-weight: var(--lk-font-weight-bold);
1709
+ }
1710
+
1711
+ .lk-ra-word[data-state="correct"] .lk-ra-word-text::before,
1712
+ .lk-pf-word[data-state="correct"] .lk-pf-word-text::before,
1713
+ .lk-ra-legend li[data-state="correct"]::before,
1714
+ .lk-pf-legend li[data-state="correct"]::before {
1715
+ content: "✓" / "";
1716
+ color: var(--lk-color-success);
1717
+ }
1718
+
1719
+ .lk-ra-word[data-state="mispronounced"] .lk-ra-word-text::before,
1720
+ .lk-pf-word[data-state="mispronounced"] .lk-pf-word-text::before,
1721
+ .lk-ra-legend li[data-state="mispronounced"]::before,
1722
+ .lk-pf-legend li[data-state="mispronounced"]::before {
1723
+ content: "✗" / "";
1724
+ color: var(--lk-color-error);
1725
+ }
1726
+
1727
+ .lk-ra-word[data-state="omitted"] .lk-ra-word-text::before,
1728
+ .lk-pf-word[data-state="omitted"] .lk-pf-word-text::before,
1729
+ .lk-ra-legend li[data-state="omitted"]::before,
1730
+ .lk-pf-legend li[data-state="omitted"]::before {
1731
+ content: "∅" / "";
1732
+ color: var(--lk-color-warning);
1733
+ }
1734
+
1735
+ .lk-ra-word[data-state="inserted"] .lk-ra-word-text::before,
1736
+ .lk-pf-word[data-state="inserted"] .lk-pf-word-text::before,
1737
+ .lk-ra-legend li[data-state="inserted"]::before,
1738
+ .lk-pf-legend li[data-state="inserted"]::before {
1739
+ content: "+" / "";
1740
+ color: var(--lk-color-text-muted);
1741
+ }
1742
+
1743
+ /* The same decoration for a word and for its legend row — one vocabulary,
1744
+ and never colour alone. */
1745
+ .lk-ra-word[data-state="correct"] .lk-ra-word-text,
1746
+ .lk-pf-word[data-state="correct"] .lk-pf-word-text,
1747
+ .lk-ra-legend li[data-state="correct"],
1748
+ .lk-pf-legend li[data-state="correct"] {
1749
+ text-decoration: none;
1750
+ color: var(--lk-color-success);
1751
+ }
1752
+
1753
+ .lk-ra-word[data-state="mispronounced"] .lk-ra-word-text,
1754
+ .lk-pf-word[data-state="mispronounced"] .lk-pf-word-text,
1755
+ .lk-ra-legend li[data-state="mispronounced"],
1756
+ .lk-pf-legend li[data-state="mispronounced"] {
1757
+ text-decoration: underline wavy;
1758
+ text-decoration-color: var(--lk-color-error);
1759
+ text-underline-offset: 0.2em;
1760
+ /* Drawn through every glyph, for the reason the dictation gives: skipping
1761
+ ink breaks the underline exactly where a mispronounced word of a stacked
1762
+ or descending script has nothing else to mark it. */
1763
+ text-decoration-skip-ink: none;
1764
+ color: var(--lk-color-error);
1765
+ }
1766
+
1767
+ .lk-ra-word[data-state="omitted"] .lk-ra-word-text,
1768
+ .lk-pf-word[data-state="omitted"] .lk-pf-word-text,
1769
+ .lk-ra-legend li[data-state="omitted"],
1770
+ .lk-pf-legend li[data-state="omitted"] {
1771
+ outline: 1px dotted var(--lk-color-warning);
1772
+ outline-offset: 1px;
1773
+ color: var(--lk-color-warning);
1774
+ }
1775
+
1776
+ .lk-ra-word[data-state="inserted"] .lk-ra-word-text,
1777
+ .lk-pf-word[data-state="inserted"] .lk-pf-word-text,
1778
+ .lk-ra-legend li[data-state="inserted"],
1779
+ .lk-pf-legend li[data-state="inserted"] {
1780
+ text-decoration: line-through;
1781
+ text-decoration-color: var(--lk-color-text-muted);
1782
+ color: var(--lk-color-text-muted);
1783
+ }
1784
+
1785
+ /* ── One word's detail ─────────────────────────────────────────────── */
1786
+ .lk-pf-word-facts {
1787
+ display: grid;
1788
+ grid-template-columns: auto 1fr;
1789
+ gap: var(--lk-spacing-xs) var(--lk-spacing-md);
1790
+ margin: 0;
1791
+ }
1792
+
1793
+ .lk-pf-word-facts dt {
1794
+ font-size: var(--lk-font-size-sm);
1795
+ color: var(--lk-color-text-muted);
1796
+ }
1797
+
1798
+ .lk-pf-word-facts dd {
1799
+ margin: 0;
1800
+ }
1801
+
1802
+ .lk-pf-phoneme {
1803
+ display: flex;
1804
+ flex-wrap: wrap;
1805
+ align-items: baseline;
1806
+ gap: var(--lk-spacing-xs);
1807
+ padding: 0 var(--lk-spacing-xs);
1808
+ border: 1px solid var(--lk-color-border);
1809
+ border-radius: var(--lk-radius-sm);
1810
+ }
1811
+
1812
+ .lk-pf-phoneme-symbol {
1813
+ font-weight: var(--lk-font-weight-bold);
1814
+ }
1815
+
1816
+ .lk-pf-heard-label {
1817
+ font-size: var(--lk-font-size-sm);
1818
+ color: var(--lk-color-text-muted);
1819
+ }
1820
+
1821
+ .lk-pf-break,
1822
+ .lk-pf-monotone,
1823
+ .lk-pf-alphabet {
1824
+ margin: 0;
1825
+ font-size: var(--lk-font-size-sm);
1826
+ color: var(--lk-color-text-muted);
1827
+ }
1828
+
1829
+ .lk-pf-break,
1830
+ .lk-pf-monotone {
1831
+ padding-inline-start: var(--lk-spacing-sm);
1832
+ border-inline-start: 3px solid var(--lk-color-warning);
1833
+ }
1834
+
1835
+ /*
1836
+ * The element the per-word play button drives. It carries no controls and is
1837
+ * never played whole, so it is given no room — and hiding it does not stop
1838
+ * it playing, which is the property this relies on.
1839
+ */
1840
+ .lk-pf-audio {
1841
+ display: none;
1842
+ }
1843
+
1844
+ .lk-ra :focus-visible,
1845
+ .lk-pf :focus-visible {
1846
+ outline: 2px solid var(--lk-color-focus-ring);
1847
+ outline-offset: 2px;
1848
+ }
1849
+
1850
+ .lk-ra > [aria-live] {
1851
+ color: var(--lk-color-text-muted);
1852
+ font-size: var(--lk-font-size-sm);
1853
+ }
1854
+
1855
+ /* ── Reduced motion (WCAG 2.3.3) ─────────────────────────────────────── */
1856
+ @media (prefers-reduced-motion: reduce) {
1857
+ .lk-ra *,
1858
+ .lk-pf *,
1859
+ .lk-ra-recorder[data-level]::after {
1860
+ transition: none;
1861
+ animation: none;
1862
+ }
1863
+ }
1864
+
1865
+ /* ── Forced colours (Windows High Contrast) ─────────────────────────────
1866
+ The system palette replaces every colour above; what must survive is the
1867
+ shape of each mark. The decorations are restated in `CanvasText` so the
1868
+ four states stay distinguishable by underline, outline, strike and glyph,
1869
+ and the blocks that mean something keep a border to be blocks at all. */
1870
+ @media (forced-colors: active) {
1871
+ .lk-ra-word-text::before,
1872
+ .lk-pf-word-text::before,
1873
+ .lk-ra-legend li[data-state]::before,
1874
+ .lk-pf-legend li[data-state]::before {
1875
+ color: CanvasText;
1876
+ }
1877
+
1878
+ .lk-ra-word[data-state="mispronounced"] .lk-ra-word-text,
1879
+ .lk-pf-word[data-state="mispronounced"] .lk-pf-word-text,
1880
+ .lk-ra-legend li[data-state="mispronounced"],
1881
+ .lk-pf-legend li[data-state="mispronounced"] {
1882
+ text-decoration: underline wavy;
1883
+ text-decoration-color: CanvasText;
1884
+ text-decoration-skip-ink: none;
1885
+ }
1886
+
1887
+ .lk-ra-word[data-state="omitted"] .lk-ra-word-text,
1888
+ .lk-pf-word[data-state="omitted"] .lk-pf-word-text,
1889
+ .lk-ra-legend li[data-state="omitted"],
1890
+ .lk-pf-legend li[data-state="omitted"] {
1891
+ outline: 1px dotted CanvasText;
1892
+ }
1893
+
1894
+ .lk-ra-word[data-state="inserted"] .lk-ra-word-text,
1895
+ .lk-pf-word[data-state="inserted"] .lk-pf-word-text,
1896
+ .lk-ra-legend li[data-state="inserted"],
1897
+ .lk-pf-legend li[data-state="inserted"] {
1898
+ text-decoration: line-through;
1899
+ text-decoration-color: CanvasText;
1900
+ }
1901
+
1902
+ .lk-ra-reference,
1903
+ .lk-ra-alert,
1904
+ .lk-ra-marks,
1905
+ .lk-pf-dimension,
1906
+ .lk-pf-word-detail,
1907
+ .lk-pf-break,
1908
+ .lk-pf-monotone {
1909
+ forced-color-adjust: none;
1910
+ border-color: CanvasText;
1911
+ color: CanvasText;
1912
+ background: Canvas;
1913
+ }
1914
+
1915
+ /* A filled bar with no border is invisible against its own background
1916
+ here, so the fill takes the system's text colour. */
1917
+ .lk-ra-recorder[data-level]::after {
1918
+ background: CanvasText;
1919
+ }
1920
+ }
1921
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intellectif/lk-react",
3
- "version": "13.0.0",
3
+ "version": "14.0.0",
4
4
  "description": "Accessible React 19 assessment components — multiple choice, fill-in-the-blanks, written response — with a resumable exam/review pager, theming, and xAPI delivery.",
5
5
  "keywords": [
6
6
  "react",
@@ -103,6 +103,26 @@
103
103
  "default": "./dist/components/Dictation.cjs"
104
104
  }
105
105
  },
106
+ "./components/ReadAloud": {
107
+ "import": {
108
+ "types": "./dist/components/ReadAloud.d.ts",
109
+ "default": "./dist/components/ReadAloud.js"
110
+ },
111
+ "require": {
112
+ "types": "./dist/components/ReadAloud.d.cts",
113
+ "default": "./dist/components/ReadAloud.cjs"
114
+ }
115
+ },
116
+ "./components/PronunciationFeedback": {
117
+ "import": {
118
+ "types": "./dist/components/PronunciationFeedback.d.ts",
119
+ "default": "./dist/components/PronunciationFeedback.js"
120
+ },
121
+ "require": {
122
+ "types": "./dist/components/PronunciationFeedback.d.cts",
123
+ "default": "./dist/components/PronunciationFeedback.cjs"
124
+ }
125
+ },
106
126
  "./components/StimulusPanel": {
107
127
  "import": {
108
128
  "types": "./dist/components/StimulusPanel.d.ts",
@@ -133,6 +153,16 @@
133
153
  "default": "./dist/hooks/useActivityState.cjs"
134
154
  }
135
155
  },
156
+ "./hooks/useSpeechRecorder": {
157
+ "import": {
158
+ "types": "./dist/hooks/useSpeechRecorder.d.ts",
159
+ "default": "./dist/hooks/useSpeechRecorder.js"
160
+ },
161
+ "require": {
162
+ "types": "./dist/hooks/useSpeechRecorder.d.cts",
163
+ "default": "./dist/hooks/useSpeechRecorder.cjs"
164
+ }
165
+ },
136
166
  "./hooks/useXAPI": {
137
167
  "import": {
138
168
  "types": "./dist/hooks/useXAPI.d.ts",
@@ -177,7 +207,7 @@
177
207
  "peerDependencies": {
178
208
  "react": "^19",
179
209
  "react-dom": "^19",
180
- "@intellectif/lk-core": "^0.13.0"
210
+ "@intellectif/lk-core": "^0.14.0"
181
211
  },
182
212
  "devDependencies": {
183
213
  "@axe-core/react": "^4.10.0",
@@ -196,7 +226,7 @@
196
226
  "tsup": "^8.3.0",
197
227
  "vitest": "^4.1.6",
198
228
  "vitest-axe": "^0.1.0",
199
- "@intellectif/lk-core": "0.13.0"
229
+ "@intellectif/lk-core": "0.14.0"
200
230
  },
201
231
  "license": "MIT",
202
232
  "engines": {