@mathscan/math-exercise-engine 1.1.15 → 1.1.16

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
@@ -68,6 +68,9 @@
68
68
  /** Margin between inline elements (0.25rem = 4px) */
69
69
  --exercise-element-margin: 0.25rem;
70
70
 
71
+ /** Minimum space kept between the exercise content and the numeric keypad */
72
+ --exercise-keypad-gap: 1rem;
73
+
71
74
  /* ========================================
72
75
  TYPOGRAPHY VARIABLES
73
76
  ======================================== */
@@ -152,6 +155,10 @@
152
155
  display: flex;
153
156
  justify-content: space-between;
154
157
  align-items: flex-start;
158
+ /* Guaranteed breathing room between the content and the keypad.
159
+ space-between alone only separates them while there is leftover
160
+ space, so wide content would otherwise run right up to the keypad. */
161
+ gap: var(--exercise-keypad-gap);
155
162
  }
156
163
 
157
164
  /* When inside a keypad layout, the exercise layout must grow to fill
@@ -319,15 +326,7 @@ mark {
319
326
  font-weight: bold;
320
327
  }
321
328
 
322
- /* ========================================
323
- IMAGE
324
- ======================================== */
325
-
326
- .image-item {
327
- vertical-align: middle;
328
- max-width: 100%;
329
- object-fit: contain;
330
- }
329
+ /* Image styles live in image.css */
331
330
 
332
331
  /* ========================================
333
332
  OPERATORS & SYMBOLS
@@ -685,11 +684,14 @@ button.drag-option {
685
684
  /**
686
685
  * Keypad container dimensions:
687
686
  * - min-width: 150px ensures keypad is always usable
688
- * - margins: 5px on each side for spacing
687
+ * - flex-shrink: 0 so the keypad keeps its size and the content
688
+ * (flex: 1; min-width: 0) absorbs the squeeze instead
689
+ * - margin-right: 5px keeps it off the outer edge; the space on its
690
+ * left comes from the container gap (--exercise-keypad-gap)
689
691
  */
690
692
  min-width: 150px;
693
+ flex-shrink: 0;
691
694
  margin-right: 5px;
692
- margin-left: 5px;
693
695
  }
694
696
 
695
697
  .numeric-keypad {
@@ -1349,6 +1351,171 @@ th.exercise-table-cell.exercise-table-cell--last-row:last-child {
1349
1351
  }
1350
1352
  }
1351
1353
 
1354
+ /**
1355
+ * Image Styles
1356
+ *
1357
+ * Inline images and their fullscreen (lightbox) view.
1358
+ * The lightbox is portalled to document.body, so its rules must not depend
1359
+ * on any ancestor inside .math-exercise-renderer.
1360
+ */
1361
+
1362
+ /* ========================================
1363
+ INLINE IMAGE
1364
+ ======================================== */
1365
+
1366
+ /**
1367
+ * Wrapper exists only for zoomable images. It is exactly the size of the
1368
+ * image — no padding, no gap, no extra track — because .exercise-line is a
1369
+ * flex row: any width the wrapper adds would push every later item along,
1370
+ * which would move a second inline image on the same line.
1371
+ */
1372
+
1373
+ .image-item-wrapper {
1374
+ position: relative;
1375
+ display: inline-block;
1376
+ /* line-height: 0 stops the inline descender gap from making the wrapper
1377
+ taller than the image it wraps. */
1378
+ line-height: 0;
1379
+ vertical-align: middle;
1380
+ }
1381
+
1382
+ .image-item {
1383
+ vertical-align: middle;
1384
+ max-width: 100%;
1385
+ object-fit: contain;
1386
+ }
1387
+
1388
+ .image-item-zoomable {
1389
+ cursor: zoom-in;
1390
+ }
1391
+
1392
+ /**
1393
+ * Expand affordance. Always visible: sitting outside the image, it costs
1394
+ * nothing to show, and a hover-reveal would be undiscoverable on touch.
1395
+ *
1396
+ * The diamond artwork carries all the chrome, so the button contributes no
1397
+ * border or background of its own. Its `color` drives the glyph, which is
1398
+ * drawn with currentColor.
1399
+ *
1400
+ * Absolutely positioned over the image's top-right corner: out of flow, so
1401
+ * it costs zero layout width.
1402
+ *
1403
+ * The overhang is deliberately small. .exercise-line is a flex row with only
1404
+ * a 0.25rem gap, so anything hanging past the image's right edge lands on the
1405
+ * next item — a live button there would swallow clicks meant for the input
1406
+ * beside the image. At 30% the badge still reads as a corner badge while the
1407
+ * part sticking out is a few pixels of tapering tip.
1408
+ *
1409
+ * clip-path does the rest: it removes the square's dead corners from the hit
1410
+ * area (clipped-away regions do not receive pointer events), which is exactly
1411
+ * where the box poked furthest into its neighbours. The polygon runs through
1412
+ * the diamond's four tips with a little tolerance for the 2px stroke, so it
1413
+ * circumscribes the artwork and clips no visible pixels.
1414
+ */
1415
+
1416
+ .image-zoom-btn {
1417
+ position: absolute;
1418
+ top: 0;
1419
+ right: 0;
1420
+ transform: translate(30%, -30%);
1421
+ clip-path: polygon(50.75% 13%, 90% 52.5%, 50.75% 92%, 11.5% 52.5%);
1422
+ display: flex;
1423
+ align-items: center;
1424
+ justify-content: center;
1425
+ flex-shrink: 0;
1426
+ /* Slightly larger than a plain square button: the diamond only fills
1427
+ ~75% of its viewBox, so the visible badge lands near 1.7rem. */
1428
+ width: 2.25rem;
1429
+ height: 2.25rem;
1430
+ padding: 0;
1431
+ border: none;
1432
+ background: none;
1433
+ color: var(--exercise-accent, #705dc0);
1434
+ line-height: 0;
1435
+ cursor: zoom-in;
1436
+ transition: transform 0.15s ease;
1437
+ }
1438
+
1439
+ .image-zoom-icon {
1440
+ width: 100%;
1441
+ height: 100%;
1442
+ display: block;
1443
+ }
1444
+
1445
+ /* The translate must be repeated here: transform is one property, so a bare
1446
+ scale() would drop the positioning and snap the badge back onto the image. */
1447
+
1448
+ .image-zoom-btn:hover,
1449
+ .image-zoom-btn:focus-visible {
1450
+ transform: translate(30%, -30%) scale(1.08);
1451
+ }
1452
+
1453
+ @media (prefers-reduced-motion: reduce) {
1454
+ .image-zoom-btn {
1455
+ transition: none;
1456
+ }
1457
+
1458
+ .image-zoom-btn:hover,
1459
+ .image-zoom-btn:focus-visible {
1460
+ transform: translate(30%, -30%);
1461
+ }
1462
+ }
1463
+
1464
+ /* ========================================
1465
+ FULLSCREEN LIGHTBOX
1466
+ ======================================== */
1467
+
1468
+ .image-lightbox {
1469
+ position: fixed;
1470
+ inset: 0;
1471
+ z-index: 1000;
1472
+ display: flex;
1473
+ align-items: center;
1474
+ justify-content: center;
1475
+ padding: 2rem;
1476
+ background: rgba(0, 0, 0, 0.85);
1477
+ cursor: zoom-out;
1478
+ }
1479
+
1480
+ .image-lightbox-img {
1481
+ max-width: 100%;
1482
+ max-height: 100%;
1483
+ object-fit: contain;
1484
+ cursor: default;
1485
+ }
1486
+
1487
+ .image-lightbox-close {
1488
+ position: absolute;
1489
+ top: 1rem;
1490
+ right: 1rem;
1491
+ display: flex;
1492
+ align-items: center;
1493
+ justify-content: center;
1494
+ width: 2.5rem;
1495
+ height: 2.5rem;
1496
+ padding: 0;
1497
+ border: none;
1498
+ border-radius: 50%;
1499
+ /* Solid white disc: the cross is the accent purple, which would be muddy
1500
+ read straight against the dark backdrop. */
1501
+ background: rgba(255, 255, 255, 0.95);
1502
+ color: var(--exercise-accent, #705dc0);
1503
+ line-height: 0;
1504
+ cursor: pointer;
1505
+ transition: background 0.15s ease;
1506
+ }
1507
+
1508
+ .image-lightbox-close-icon {
1509
+ width: 1.5rem;
1510
+ height: 1.5rem;
1511
+ display: block;
1512
+ }
1513
+
1514
+ .image-lightbox-close:hover,
1515
+ .image-lightbox-close:focus-visible {
1516
+ background: #ffffff;
1517
+ }
1518
+
1352
1519
  /**
1353
1520
  * Number Line Styles
1354
1521
  *
@@ -2316,6 +2483,11 @@ th.exercise-table-cell.exercise-table-cell--last-row:last-child {
2316
2483
  /** Reduce gap between keypad buttons */
2317
2484
  gap: var(--exercise-element-margin);
2318
2485
  }
2486
+
2487
+ .exercise-layout-container.has-keypad {
2488
+ /** Tighter separation between content and keypad on narrow screens */
2489
+ gap: calc(var(--exercise-keypad-gap) * 0.5);
2490
+ }
2319
2491
  }
2320
2492
 
2321
2493
  /* ========================================
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mathscan/math-exercise-engine",
3
- "version": "1.1.15",
3
+ "version": "1.1.16",
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",