@mathscan/math-exercise-engine 1.1.7 → 1.1.9

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
  /* ========================================
@@ -1255,7 +1257,64 @@ th.exercise-table-cell {
1255
1257
  }
1256
1258
 
1257
1259
  /* ========================================
1258
- ROW 2 — AXIS (track + ticks + arrows + dots)
1260
+ ROW 2 — ARROW ANNOTATION ARCS
1261
+ ======================================== */
1262
+
1263
+ .number-line-arrow-arcs-row {
1264
+ position: relative;
1265
+ width: 100%;
1266
+ margin-top: 0.1rem;
1267
+ }
1268
+
1269
+ .number-line-arrow-arc {
1270
+ position: absolute;
1271
+ height: 80px;
1272
+ pointer-events: none;
1273
+ }
1274
+
1275
+ .number-line-arrow-arc-svg {
1276
+ position: absolute;
1277
+ left: 0;
1278
+ right: 0;
1279
+ bottom: 0;
1280
+ width: 100%;
1281
+ height: 64px;
1282
+ overflow: visible;
1283
+ pointer-events: none;
1284
+ }
1285
+
1286
+ .number-line-arrow-arc-path {
1287
+ fill: none;
1288
+ stroke: var(--nl-arrow-arc-color);
1289
+ stroke-width: var(--nl-arrow-arc-stroke-width);
1290
+ stroke-linecap: round;
1291
+ stroke-linejoin: round;
1292
+ vector-effect: non-scaling-stroke;
1293
+ }
1294
+
1295
+ .number-line-arrow-arc-head {
1296
+ fill: var(--nl-arrow-arc-color);
1297
+ stroke: none;
1298
+ }
1299
+
1300
+ .number-line-arrow-arc-label {
1301
+ position: absolute;
1302
+ /* top is always set via inline style from getArrowArcLabelTop() */
1303
+ left: 50%;
1304
+ transform: translateX(-50%);
1305
+ color: var(--nl-arrow-arc-color);
1306
+ font-size: calc(var(--exercise-font-size) * 0.8);
1307
+ font-weight: var(--exercise-font-weight-bold, 600);
1308
+ line-height: 1.1;
1309
+ pointer-events: auto;
1310
+ /* Centre content horizontally; allow LayoutRenderer to size naturally */
1311
+ display: flex;
1312
+ align-items: center;
1313
+ justify-content: center;
1314
+ }
1315
+
1316
+ /* ========================================
1317
+ ROW 3 — AXIS (track + ticks + arrows + dots)
1259
1318
  ======================================== */
1260
1319
 
1261
1320
  .number-line-axis-row {
@@ -1264,6 +1323,10 @@ th.exercise-table-cell {
1264
1323
  height: 2rem;
1265
1324
  }
1266
1325
 
1326
+ .number-line-axis-row:has(.number-line-cursor-label) {
1327
+ height: 3rem;
1328
+ }
1329
+
1267
1330
  /* In drag mode, chip zone is handled by .number-line--drag-mode padding-top on root */
1268
1331
 
1269
1332
  /* Horizontal track line — spans [--nl-side-pad, 100% - --nl-side-pad] by default */
@@ -1543,7 +1606,7 @@ th.exercise-table-cell {
1543
1606
  /* Axis glow on drag-over intentionally disabled — axis stays neutral */
1544
1607
 
1545
1608
  /* ========================================
1546
- ROW 3 — NUMERIC TICK LABELS
1609
+ ROW 4 — NUMERIC TICK LABELS
1547
1610
  ======================================== */
1548
1611
 
1549
1612
  .number-line-labels-row {
@@ -1563,7 +1626,7 @@ th.exercise-table-cell {
1563
1626
  }
1564
1627
 
1565
1628
  /* ========================================
1566
- HIGHLIGHT BANDS
1629
+ ROW 5 — HIGHLIGHT BANDS / LABELS
1567
1630
  ======================================== */
1568
1631
 
1569
1632
  /* A coloured band spanning [from%, to%] on the axis row */
@@ -1598,84 +1661,197 @@ th.exercise-table-cell {
1598
1661
  pointer-events: none;
1599
1662
  }
1600
1663
 
1664
+ /* ========================================
1665
+ CURSOR BALL
1666
+ ======================================== */
1667
+
1668
+ /* Filled range between the cursor's initial value and its current position */
1669
+
1670
+ .number-line-cursor-fill {
1671
+ position: absolute;
1672
+ top: 50%;
1673
+ transform: translateY(-50%);
1674
+ height: 6px;
1675
+ background-color: var(--nl-cursor-color, var(--exercise-accent, #705DC0));
1676
+ opacity: 0.28;
1677
+ border-radius: 999px;
1678
+ pointer-events: none;
1679
+ z-index: 2;
1680
+ }
1681
+
1682
+ /* The ball rendered at the cursor's current value */
1683
+
1684
+ .number-line-cursor-ball {
1685
+ position: absolute;
1686
+ top: 50%;
1687
+ transform: translate(-50%, -50%);
1688
+ width: 16px;
1689
+ height: 16px;
1690
+ border-radius: 50%;
1691
+ background-color: #fff;
1692
+ border: 2.5px solid var(--nl-cursor-color, var(--exercise-accent, #705DC0));
1693
+ box-shadow: 0 1px 4px rgba(0, 0, 0, 0.18);
1694
+ pointer-events: auto;
1695
+ cursor: grab;
1696
+ z-index: 4;
1697
+ touch-action: none; /* required for pointer capture on mobile */
1698
+ transition: box-shadow 0.12s ease, transform 0.12s ease;
1699
+ }
1700
+
1701
+ .number-line-cursor-ball:hover {
1702
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.28);
1703
+ transform: translate(-50%, -50%) scale(1.15);
1704
+ }
1705
+
1706
+ .number-line-cursor-ball--dragging {
1707
+ cursor: grabbing;
1708
+ box-shadow: 0 4px 14px rgba(0, 0, 0, 0.32);
1709
+ transform: translate(-50%, -50%) scale(1.25);
1710
+ transition: none;
1711
+ }
1712
+
1713
+ @keyframes number-line-cursor-jiggle {
1714
+ 0%, 100% { transform: translate(-50%, -50%) translateX(0) rotate(0deg); }
1715
+ 20% { transform: translate(-50%, -50%) translateX(-5px) rotate(-2deg); }
1716
+ 40% { transform: translate(-50%, -50%) translateX(5px) rotate(2deg); }
1717
+ 60% { transform: translate(-50%, -50%) translateX(-3px) rotate(-1deg); }
1718
+ 80% { transform: translate(-50%, -50%) translateX(3px) rotate(1deg); }
1719
+ }
1720
+
1721
+ .number-line-cursor-ball--jiggle {
1722
+ animation: number-line-cursor-jiggle 0.55s ease-in-out 0.4s 2 both;
1723
+ }
1724
+
1725
+ .number-line-cursor-label {
1726
+ position: absolute;
1727
+ top: calc(50% - 0.6rem);
1728
+ display: block;
1729
+ max-width: min(8rem, 26vw);
1730
+ transform: translate(-50%, -100%);
1731
+ overflow: hidden;
1732
+ color: var(--nl-cursor-color, var(--exercise-text-light, #555));
1733
+ font-size: calc(var(--exercise-font-size) * 0.75);
1734
+ font-weight: var(--exercise-font-weight-bold, 600);
1735
+ line-height: 1.1;
1736
+ pointer-events: none;
1737
+ text-align: center;
1738
+ text-overflow: ellipsis;
1739
+ white-space: nowrap;
1740
+ z-index: 5;
1741
+ }
1742
+
1743
+ /* Validation feedback — mirrors the correct/incorrect axis colors */
1744
+
1745
+ .number-line-cursor-ball--correct,
1746
+ .number-line-cursor-fill--correct,
1747
+ .number-line-cursor-label--correct {
1748
+ --nl-cursor-color: #43B925;
1749
+ }
1750
+
1751
+ .number-line-cursor-ball--incorrect,
1752
+ .number-line-cursor-fill--incorrect,
1753
+ .number-line-cursor-label--incorrect {
1754
+ --nl-cursor-color: #D6234D;
1755
+ }
1756
+
1757
+ .number-line-cursor-ball:focus-visible {
1758
+ outline: 2px solid var(--nl-cursor-color, var(--exercise-accent, #705DC0));
1759
+ outline-offset: 3px;
1760
+ }
1761
+
1601
1762
  /* ========================================
1602
1763
  COUNTER UI
1764
+ ROW 6 — COUNTER UI
1603
1765
  ======================================== */
1604
1766
 
1605
1767
  .number-line-counter {
1768
+ /* ── Color tokens (override to restyle) ── */
1769
+ --nl-counter-bg: #FFFFFF;
1770
+ --nl-counter-border-color: #FFCA76;
1771
+ --nl-counter-value-color: #705DC0;
1772
+ --nl-counter-btn-bg: #E7E2FF;
1773
+ --nl-counter-btn-color: #705DC0;
1774
+
1775
+ /* ── Layout ── */
1606
1776
  display: flex;
1607
1777
  flex-direction: column;
1608
1778
  align-items: center;
1609
1779
  gap: 0.25rem;
1610
- margin-top: 0.5rem;
1780
+ margin: 0.5rem auto 0;
1611
1781
  padding: 0.4rem 0.75rem;
1612
1782
  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;
1783
+ box-sizing: border-box;
1784
+ background: var(--nl-counter-bg);
1785
+ border: 1px solid var(--nl-counter-border-color);
1786
+ border-radius: 9px;
1618
1787
  }
1619
1788
 
1620
- /* Inner row: − value + buttons as a cohesive stepper */
1789
+ .number-line-counter-label {
1790
+ font-size: calc(var(--exercise-font-size) * 0.85);
1791
+ color: var(--exercise-text-light, #555);
1792
+ text-align: center;
1793
+ }
1621
1794
 
1622
1795
  .number-line-counter-controls {
1623
1796
  display: flex;
1624
1797
  align-items: center;
1625
1798
  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;
1799
+ gap: 0.75rem;
1633
1800
  }
1634
1801
 
1635
1802
  .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);
1803
+ display: flex;
1804
+ align-items: center;
1805
+ justify-content: center;
1640
1806
  min-width: 2ch;
1641
- text-align: center;
1807
+ font-size: calc(var(--exercise-font-size) * 2);
1808
+ font-weight: 700;
1809
+ font-variant-numeric: tabular-nums;
1642
1810
  line-height: 1;
1811
+ text-align: center;
1812
+ color: var(--nl-counter-value-color);
1643
1813
  }
1644
1814
 
1645
1815
  .number-line-counter-btn {
1816
+ position: relative;
1646
1817
  display: inline-flex;
1647
1818
  align-items: center;
1648
1819
  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;
1820
+ width: 48px;
1821
+ height: 48px;
1822
+ border: none;
1823
+ border-radius: 9px;
1824
+ background: var(--nl-counter-btn-bg);
1825
+ color: var(--nl-counter-btn-color);
1826
+ font-size: 0;
1660
1827
  cursor: pointer;
1661
- transition: all 0.12s ease;
1828
+ transition: filter 0.12s ease, transform 0.12s ease;
1662
1829
  -webkit-tap-highlight-color: transparent;
1663
1830
  }
1664
1831
 
1665
- .number-line-counter-btn:hover:not(:disabled) {
1666
- background: var(--exercise-accent, #705DC0);
1667
- color: white;
1668
- }
1832
+ .number-line-counter-btn:hover:not(:disabled) { filter: brightness(0.94); }
1669
1833
 
1670
- .number-line-counter-btn:active:not(:disabled) {
1671
- transform: scale(0.93);
1672
- }
1834
+ .number-line-counter-btn:active:not(:disabled) { transform: scale(0.93); }
1835
+
1836
+ .number-line-counter-btn:disabled { opacity: 0.45; cursor: not-allowed; }
1673
1837
 
1674
- .number-line-counter-btn:disabled {
1675
- opacity: 0.35;
1676
- cursor: not-allowed;
1838
+ /* ── Icon pseudo-elements (horizontal bar shared; vertical bar on + only) ── */
1839
+
1840
+ .number-line-counter-btn::before,
1841
+ .number-line-counter-btn::after {
1842
+ content: '';
1843
+ position: absolute;
1844
+ left: 50%;
1845
+ top: 50%;
1846
+ background: currentColor;
1847
+ border-radius: 999px;
1848
+ transform: translate(-50%, -50%);
1677
1849
  }
1678
1850
 
1851
+ .number-line-counter-btn::before { width: 29px; height: 7px; }
1852
+
1853
+ .number-line-counter-btn[aria-label='Increase']::after { width: 7px; height: 29px; }
1854
+
1679
1855
  /* ========================================
1680
1856
  ACCESSIBILITY — FOCUS-VISIBLE RINGS (item 6)
1681
1857
  ======================================== */
@@ -2132,6 +2308,19 @@ th.exercise-table-cell {
2132
2308
  overflow: visible;
2133
2309
  }
2134
2310
 
2311
+ /* ── Intro jiggle — plays on mount so students know tools are draggable ─────── */
2312
+ @keyframes tool-jiggle {
2313
+ 0%, 100% { transform: translateX(0) rotate(0deg); }
2314
+ 20% { transform: translateX(-5px) rotate(-2deg); }
2315
+ 40% { transform: translateX(5px) rotate(2deg); }
2316
+ 60% { transform: translateX(-3px) rotate(-1deg); }
2317
+ 80% { transform: translateX(3px) rotate(1deg); }
2318
+ }
2319
+
2320
+ .tool-content-jiggle {
2321
+ animation: tool-jiggle 0.55s ease-in-out 0.4s 2 both;
2322
+ }
2323
+
2135
2324
  /* ── Flip button (set-square mirror) ─────────────────────────────────────────── */
2136
2325
  .tool-flip-btn {
2137
2326
  position: absolute;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mathscan/math-exercise-engine",
3
- "version": "1.1.7",
3
+ "version": "1.1.9",
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",