@mathscan/math-exercise-engine 1.1.8 → 1.1.10

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/dist/style.css CHANGED
@@ -1206,6 +1206,8 @@ th.exercise-table-cell {
1206
1206
  --nl-visual-fraction-size: clamp(5.5rem, 18vw, 8rem);
1207
1207
  /* Visual gap between end ticks and arrowheads — pure CSS, no effect on calculations */
1208
1208
  --nl-arrow-gap: 8px;
1209
+ --nl-arrow-arc-color: var(--exercise-danger, #D6234D);
1210
+ --nl-arrow-arc-stroke-width: 1.75;
1209
1211
  }
1210
1212
 
1211
1213
  /* ========================================
@@ -1230,7 +1232,39 @@ th.exercise-table-cell {
1230
1232
  }
1231
1233
 
1232
1234
  /* ========================================
1233
- ROW 1 — POSITION CONTENT (inputs / labels above axis)
1235
+ ROW 1 — OVERLAYS ATTACHED TO AXIS
1236
+ ======================================== */
1237
+
1238
+ .number-line-overlays-row {
1239
+ position: relative;
1240
+ width: 100%;
1241
+ min-height: max(
1242
+ var(--nl-position-content-min-height),
1243
+ var(--nl-position-content-height),
1244
+ var(--nl-arrow-arcs-row-height, 0px)
1245
+ );
1246
+ margin-top: 0.1rem;
1247
+ }
1248
+
1249
+ .number-line-overlays-row .number-line-content-row,
1250
+ .number-line-overlays-row .number-line-arrow-arcs-row {
1251
+ position: absolute;
1252
+ left: 0;
1253
+ right: 0;
1254
+ bottom: 0;
1255
+ margin-top: 0;
1256
+ }
1257
+
1258
+ /* The two overlays share the same axis-anchored box. Position content holds the
1259
+ interactive inputs, so it takes pointer/paint priority over arc annotations —
1260
+ made explicit here rather than relying on DOM sibling order. */
1261
+
1262
+ .number-line-overlays-row .number-line-arrow-arcs-row { z-index: 0; }
1263
+
1264
+ .number-line-overlays-row .number-line-content-row { z-index: 1; }
1265
+
1266
+ /* ========================================
1267
+ POSITION CONTENT (inputs / labels above axis)
1234
1268
  ======================================== */
1235
1269
 
1236
1270
  .number-line-content-row {
@@ -1255,7 +1289,64 @@ th.exercise-table-cell {
1255
1289
  }
1256
1290
 
1257
1291
  /* ========================================
1258
- ROW 2 — AXIS (track + ticks + arrows + dots)
1292
+ ARROW ANNOTATION ARCS
1293
+ ======================================== */
1294
+
1295
+ .number-line-arrow-arcs-row {
1296
+ position: relative;
1297
+ width: 100%;
1298
+ margin-top: 0.1rem;
1299
+ }
1300
+
1301
+ .number-line-arrow-arc {
1302
+ position: absolute;
1303
+ height: 80px;
1304
+ pointer-events: none;
1305
+ }
1306
+
1307
+ .number-line-arrow-arc-svg {
1308
+ position: absolute;
1309
+ left: 0;
1310
+ right: 0;
1311
+ bottom: 0;
1312
+ width: 100%;
1313
+ height: 64px;
1314
+ overflow: visible;
1315
+ pointer-events: none;
1316
+ }
1317
+
1318
+ .number-line-arrow-arc-path {
1319
+ fill: none;
1320
+ stroke: var(--nl-arrow-arc-color);
1321
+ stroke-width: var(--nl-arrow-arc-stroke-width);
1322
+ stroke-linecap: round;
1323
+ stroke-linejoin: round;
1324
+ vector-effect: non-scaling-stroke;
1325
+ }
1326
+
1327
+ .number-line-arrow-arc-head {
1328
+ fill: var(--nl-arrow-arc-color);
1329
+ stroke: none;
1330
+ }
1331
+
1332
+ .number-line-arrow-arc-label {
1333
+ position: absolute;
1334
+ /* top is always set via inline style from getArrowArcLabelTop() */
1335
+ left: 50%;
1336
+ transform: translateX(-50%);
1337
+ color: var(--nl-arrow-arc-color);
1338
+ font-size: calc(var(--exercise-font-size) * 0.8);
1339
+ font-weight: var(--exercise-font-weight-bold, 600);
1340
+ line-height: 1.1;
1341
+ pointer-events: auto;
1342
+ /* Centre content horizontally; allow LayoutRenderer to size naturally */
1343
+ display: flex;
1344
+ align-items: center;
1345
+ justify-content: center;
1346
+ }
1347
+
1348
+ /* ========================================
1349
+ ROW 3 — AXIS (track + ticks + arrows + dots)
1259
1350
  ======================================== */
1260
1351
 
1261
1352
  .number-line-axis-row {
@@ -1264,6 +1355,10 @@ th.exercise-table-cell {
1264
1355
  height: 2rem;
1265
1356
  }
1266
1357
 
1358
+ .number-line-axis-row:has(.number-line-cursor-label) {
1359
+ height: 3rem;
1360
+ }
1361
+
1267
1362
  /* In drag mode, chip zone is handled by .number-line--drag-mode padding-top on root */
1268
1363
 
1269
1364
  /* Horizontal track line — spans [--nl-side-pad, 100% - --nl-side-pad] by default */
@@ -1543,7 +1638,7 @@ th.exercise-table-cell {
1543
1638
  /* Axis glow on drag-over intentionally disabled — axis stays neutral */
1544
1639
 
1545
1640
  /* ========================================
1546
- ROW 3 — NUMERIC TICK LABELS
1641
+ ROW 4 — NUMERIC TICK LABELS
1547
1642
  ======================================== */
1548
1643
 
1549
1644
  .number-line-labels-row {
@@ -1563,7 +1658,7 @@ th.exercise-table-cell {
1563
1658
  }
1564
1659
 
1565
1660
  /* ========================================
1566
- HIGHLIGHT BANDS
1661
+ ROW 5 — HIGHLIGHT BANDS / LABELS
1567
1662
  ======================================== */
1568
1663
 
1569
1664
  /* A coloured band spanning [from%, to%] on the axis row */
@@ -1598,84 +1693,197 @@ th.exercise-table-cell {
1598
1693
  pointer-events: none;
1599
1694
  }
1600
1695
 
1696
+ /* ========================================
1697
+ CURSOR BALL
1698
+ ======================================== */
1699
+
1700
+ /* Filled range between the cursor's initial value and its current position */
1701
+
1702
+ .number-line-cursor-fill {
1703
+ position: absolute;
1704
+ top: 50%;
1705
+ transform: translateY(-50%);
1706
+ height: 6px;
1707
+ background-color: var(--nl-cursor-color, var(--exercise-accent, #705DC0));
1708
+ opacity: 0.28;
1709
+ border-radius: 999px;
1710
+ pointer-events: none;
1711
+ z-index: 2;
1712
+ }
1713
+
1714
+ /* The ball rendered at the cursor's current value */
1715
+
1716
+ .number-line-cursor-ball {
1717
+ position: absolute;
1718
+ top: 50%;
1719
+ transform: translate(-50%, -50%);
1720
+ width: 16px;
1721
+ height: 16px;
1722
+ border-radius: 50%;
1723
+ background-color: #fff;
1724
+ border: 2.5px solid var(--nl-cursor-color, var(--exercise-accent, #705DC0));
1725
+ box-shadow: 0 1px 4px rgba(0, 0, 0, 0.18);
1726
+ pointer-events: auto;
1727
+ cursor: grab;
1728
+ z-index: 4;
1729
+ touch-action: none; /* required for pointer capture on mobile */
1730
+ transition: box-shadow 0.12s ease, transform 0.12s ease;
1731
+ }
1732
+
1733
+ .number-line-cursor-ball:hover {
1734
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.28);
1735
+ transform: translate(-50%, -50%) scale(1.15);
1736
+ }
1737
+
1738
+ .number-line-cursor-ball--dragging {
1739
+ cursor: grabbing;
1740
+ box-shadow: 0 4px 14px rgba(0, 0, 0, 0.32);
1741
+ transform: translate(-50%, -50%) scale(1.25);
1742
+ transition: none;
1743
+ }
1744
+
1745
+ @keyframes number-line-cursor-jiggle {
1746
+ 0%, 100% { transform: translate(-50%, -50%) translateX(0) rotate(0deg); }
1747
+ 20% { transform: translate(-50%, -50%) translateX(-5px) rotate(-2deg); }
1748
+ 40% { transform: translate(-50%, -50%) translateX(5px) rotate(2deg); }
1749
+ 60% { transform: translate(-50%, -50%) translateX(-3px) rotate(-1deg); }
1750
+ 80% { transform: translate(-50%, -50%) translateX(3px) rotate(1deg); }
1751
+ }
1752
+
1753
+ .number-line-cursor-ball--jiggle {
1754
+ animation: number-line-cursor-jiggle 0.55s ease-in-out 0.4s 2 both;
1755
+ }
1756
+
1757
+ .number-line-cursor-label {
1758
+ position: absolute;
1759
+ top: calc(50% - 0.6rem);
1760
+ display: block;
1761
+ max-width: min(8rem, 26vw);
1762
+ transform: translate(-50%, -100%);
1763
+ overflow: hidden;
1764
+ color: var(--nl-cursor-color, var(--exercise-text-light, #555));
1765
+ font-size: calc(var(--exercise-font-size) * 0.75);
1766
+ font-weight: var(--exercise-font-weight-bold, 600);
1767
+ line-height: 1.1;
1768
+ pointer-events: none;
1769
+ text-align: center;
1770
+ text-overflow: ellipsis;
1771
+ white-space: nowrap;
1772
+ z-index: 5;
1773
+ }
1774
+
1775
+ /* Validation feedback — mirrors the correct/incorrect axis colors */
1776
+
1777
+ .number-line-cursor-ball--correct,
1778
+ .number-line-cursor-fill--correct,
1779
+ .number-line-cursor-label--correct {
1780
+ --nl-cursor-color: #43B925;
1781
+ }
1782
+
1783
+ .number-line-cursor-ball--incorrect,
1784
+ .number-line-cursor-fill--incorrect,
1785
+ .number-line-cursor-label--incorrect {
1786
+ --nl-cursor-color: #D6234D;
1787
+ }
1788
+
1789
+ .number-line-cursor-ball:focus-visible {
1790
+ outline: 2px solid var(--nl-cursor-color, var(--exercise-accent, #705DC0));
1791
+ outline-offset: 3px;
1792
+ }
1793
+
1601
1794
  /* ========================================
1602
1795
  COUNTER UI
1796
+ ROW 6 — COUNTER UI
1603
1797
  ======================================== */
1604
1798
 
1605
1799
  .number-line-counter {
1800
+ /* ── Color tokens (override to restyle) ── */
1801
+ --nl-counter-bg: #FFFFFF;
1802
+ --nl-counter-border-color: #FFCA76;
1803
+ --nl-counter-value-color: #705DC0;
1804
+ --nl-counter-btn-bg: #E7E2FF;
1805
+ --nl-counter-btn-color: #705DC0;
1806
+
1807
+ /* ── Layout ── */
1606
1808
  display: flex;
1607
1809
  flex-direction: column;
1608
1810
  align-items: center;
1609
1811
  gap: 0.25rem;
1610
- margin-top: 0.5rem;
1812
+ margin: 0.5rem auto 0;
1611
1813
  padding: 0.4rem 0.75rem;
1612
1814
  width: fit-content;
1613
- margin-left: auto;
1614
- margin-right: auto;
1615
- background: var(--exercise-bg, white);
1616
- border: 1px solid var(--exercise-border, #ddd);
1617
- border-radius: 8px;
1815
+ box-sizing: border-box;
1816
+ background: var(--nl-counter-bg);
1817
+ border: 1px solid var(--nl-counter-border-color);
1818
+ border-radius: 9px;
1618
1819
  }
1619
1820
 
1620
- /* Inner row: − value + buttons as a cohesive stepper */
1821
+ .number-line-counter-label {
1822
+ font-size: calc(var(--exercise-font-size) * 0.85);
1823
+ color: var(--exercise-text-light, #555);
1824
+ text-align: center;
1825
+ }
1621
1826
 
1622
1827
  .number-line-counter-controls {
1623
1828
  display: flex;
1624
1829
  align-items: center;
1625
1830
  justify-content: center;
1626
- gap: 0.5rem;
1627
- }
1628
-
1629
- .number-line-counter-label {
1630
- font-size: calc(var(--exercise-font-size) * 0.85);
1631
- color: var(--exercise-text-light, #555);
1632
- text-align: center;
1831
+ gap: 0.75rem;
1633
1832
  }
1634
1833
 
1635
1834
  .number-line-counter-value {
1636
- font-size: calc(var(--exercise-font-size) * 1.15);
1637
- font-weight: 600;
1638
- font-variant-numeric: tabular-nums;
1639
- color: var(--exercise-text, #333);
1835
+ display: flex;
1836
+ align-items: center;
1837
+ justify-content: center;
1640
1838
  min-width: 2ch;
1641
- text-align: center;
1839
+ font-size: calc(var(--exercise-font-size) * 2);
1840
+ font-weight: 700;
1841
+ font-variant-numeric: tabular-nums;
1642
1842
  line-height: 1;
1843
+ text-align: center;
1844
+ color: var(--nl-counter-value-color);
1643
1845
  }
1644
1846
 
1645
1847
  .number-line-counter-btn {
1848
+ position: relative;
1646
1849
  display: inline-flex;
1647
1850
  align-items: center;
1648
1851
  justify-content: center;
1649
- min-width: 36px;
1650
- min-height: 36px;
1651
- width: 36px;
1652
- height: 36px;
1653
- border: 1.5px solid var(--exercise-accent, #705DC0);
1654
- border-radius: 50%;
1655
- background: transparent;
1656
- color: var(--exercise-accent, #705DC0);
1657
- font-size: 1.1rem;
1658
- font-weight: 500;
1659
- line-height: 1;
1852
+ width: 48px;
1853
+ height: 48px;
1854
+ border: none;
1855
+ border-radius: 9px;
1856
+ background: var(--nl-counter-btn-bg);
1857
+ color: var(--nl-counter-btn-color);
1858
+ font-size: 0;
1660
1859
  cursor: pointer;
1661
- transition: all 0.12s ease;
1860
+ transition: filter 0.12s ease, transform 0.12s ease;
1662
1861
  -webkit-tap-highlight-color: transparent;
1663
1862
  }
1664
1863
 
1665
- .number-line-counter-btn:hover:not(:disabled) {
1666
- background: var(--exercise-accent, #705DC0);
1667
- color: white;
1668
- }
1864
+ .number-line-counter-btn:hover:not(:disabled) { filter: brightness(0.94); }
1669
1865
 
1670
- .number-line-counter-btn:active:not(:disabled) {
1671
- transform: scale(0.93);
1672
- }
1866
+ .number-line-counter-btn:active:not(:disabled) { transform: scale(0.93); }
1673
1867
 
1674
- .number-line-counter-btn:disabled {
1675
- opacity: 0.35;
1676
- cursor: not-allowed;
1868
+ .number-line-counter-btn:disabled { opacity: 0.45; cursor: not-allowed; }
1869
+
1870
+ /* ── Icon pseudo-elements (horizontal bar shared; vertical bar on + only) ── */
1871
+
1872
+ .number-line-counter-btn::before,
1873
+ .number-line-counter-btn::after {
1874
+ content: '';
1875
+ position: absolute;
1876
+ left: 50%;
1877
+ top: 50%;
1878
+ background: currentColor;
1879
+ border-radius: 999px;
1880
+ transform: translate(-50%, -50%);
1677
1881
  }
1678
1882
 
1883
+ .number-line-counter-btn::before { width: 29px; height: 7px; }
1884
+
1885
+ .number-line-counter-btn[aria-label='Increase']::after { width: 7px; height: 29px; }
1886
+
1679
1887
  /* ========================================
1680
1888
  ACCESSIBILITY — FOCUS-VISIBLE RINGS (item 6)
1681
1889
  ======================================== */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mathscan/math-exercise-engine",
3
- "version": "1.1.8",
3
+ "version": "1.1.10",
4
4
  "description": "A DSL parser and validator for math exercises - transforms text-based exercise definitions into structured AST and JSON",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",