@mathscan/math-exercise-engine 1.1.15 → 1.1.17
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/index.d.ts +940 -248
- package/dist/index.js +762 -45
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +763 -46
- package/dist/index.mjs.map +1 -1
- package/dist/style.css +278 -11
- package/package.json +2 -1
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
|
-
* -
|
|
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
|
*
|
|
@@ -2251,6 +2418,101 @@ th.exercise-table-cell.exercise-table-cell--last-row:last-child {
|
|
|
2251
2418
|
background: #fef2f2;
|
|
2252
2419
|
}
|
|
2253
2420
|
|
|
2421
|
+
.grid-symmetry {
|
|
2422
|
+
/* Figma palette. All tokens remain overridable by consuming applications. */
|
|
2423
|
+
--grid-symmetry-cell-color: #fff;
|
|
2424
|
+
--grid-symmetry-focus-color: #0f172a;
|
|
2425
|
+
--grid-symmetry-reference-color: #705dc0;
|
|
2426
|
+
--grid-symmetry-prefilled-color: #4f86d9;
|
|
2427
|
+
--grid-symmetry-answer-color: #291a64;
|
|
2428
|
+
--grid-symmetry-correct-color: #43b925;
|
|
2429
|
+
--grid-symmetry-incorrect-color: #d6234d;
|
|
2430
|
+
--grid-symmetry-line-color: #ffca76;
|
|
2431
|
+
--grid-symmetry-axis-color: #291a64;
|
|
2432
|
+
max-width: 100%;
|
|
2433
|
+
overflow: auto;
|
|
2434
|
+
padding: 0.75rem;
|
|
2435
|
+
}
|
|
2436
|
+
|
|
2437
|
+
.grid-symmetry__grid {
|
|
2438
|
+
display: grid;
|
|
2439
|
+
position: relative;
|
|
2440
|
+
width: max-content;
|
|
2441
|
+
gap: 1px;
|
|
2442
|
+
padding: 1px;
|
|
2443
|
+
background: var(--grid-symmetry-line-color);
|
|
2444
|
+
}
|
|
2445
|
+
|
|
2446
|
+
.grid-symmetry__cell {
|
|
2447
|
+
box-sizing: border-box;
|
|
2448
|
+
width: var(--grid-symmetry-cell-size);
|
|
2449
|
+
height: var(--grid-symmetry-cell-size);
|
|
2450
|
+
min-width: 0;
|
|
2451
|
+
padding: 0;
|
|
2452
|
+
border: 0;
|
|
2453
|
+
border-radius: 0;
|
|
2454
|
+
background: var(--grid-symmetry-cell-color);
|
|
2455
|
+
appearance: none;
|
|
2456
|
+
opacity: 1;
|
|
2457
|
+
transition: background-color 120ms ease;
|
|
2458
|
+
}
|
|
2459
|
+
|
|
2460
|
+
/* Unlocked cells only — locked ones render as plain spans and are not clickable. */
|
|
2461
|
+
|
|
2462
|
+
.grid-symmetry__cell--editable {
|
|
2463
|
+
cursor: pointer;
|
|
2464
|
+
}
|
|
2465
|
+
|
|
2466
|
+
.grid-symmetry__cell--editable:focus-visible {
|
|
2467
|
+
outline: 3px solid var(--grid-symmetry-focus-color);
|
|
2468
|
+
outline-offset: -3px;
|
|
2469
|
+
position: relative;
|
|
2470
|
+
z-index: 2;
|
|
2471
|
+
}
|
|
2472
|
+
|
|
2473
|
+
.grid-symmetry__cell--reference {
|
|
2474
|
+
background: var(--grid-symmetry-reference-color);
|
|
2475
|
+
}
|
|
2476
|
+
|
|
2477
|
+
.grid-symmetry__cell--prefilled {
|
|
2478
|
+
background: var(--grid-symmetry-prefilled-color);
|
|
2479
|
+
}
|
|
2480
|
+
|
|
2481
|
+
.grid-symmetry__cell--answer {
|
|
2482
|
+
background: var(--grid-symmetry-answer-color);
|
|
2483
|
+
}
|
|
2484
|
+
|
|
2485
|
+
.grid-symmetry--correct .grid-symmetry__cell--interactive.grid-symmetry__cell--selected {
|
|
2486
|
+
background: var(--grid-symmetry-correct-color);
|
|
2487
|
+
}
|
|
2488
|
+
|
|
2489
|
+
.grid-symmetry--incorrect .grid-symmetry__cell--interactive.grid-symmetry__cell--selected {
|
|
2490
|
+
background: var(--grid-symmetry-incorrect-color);
|
|
2491
|
+
}
|
|
2492
|
+
|
|
2493
|
+
.grid-symmetry__axis {
|
|
2494
|
+
position: absolute;
|
|
2495
|
+
z-index: 3;
|
|
2496
|
+
pointer-events: none;
|
|
2497
|
+
background: var(--grid-symmetry-axis-color);
|
|
2498
|
+
}
|
|
2499
|
+
|
|
2500
|
+
.grid-symmetry__axis--vertical {
|
|
2501
|
+
top: -12px;
|
|
2502
|
+
bottom: -12px;
|
|
2503
|
+
left: 50%;
|
|
2504
|
+
width: 3px;
|
|
2505
|
+
transform: translateX(-50%);
|
|
2506
|
+
}
|
|
2507
|
+
|
|
2508
|
+
.grid-symmetry__axis--horizontal {
|
|
2509
|
+
right: -12px;
|
|
2510
|
+
left: -12px;
|
|
2511
|
+
top: 50%;
|
|
2512
|
+
height: 3px;
|
|
2513
|
+
transform: translateY(-50%);
|
|
2514
|
+
}
|
|
2515
|
+
|
|
2254
2516
|
/**
|
|
2255
2517
|
* Button & Action Styles
|
|
2256
2518
|
*
|
|
@@ -2316,6 +2578,11 @@ th.exercise-table-cell.exercise-table-cell--last-row:last-child {
|
|
|
2316
2578
|
/** Reduce gap between keypad buttons */
|
|
2317
2579
|
gap: var(--exercise-element-margin);
|
|
2318
2580
|
}
|
|
2581
|
+
|
|
2582
|
+
.exercise-layout-container.has-keypad {
|
|
2583
|
+
/** Tighter separation between content and keypad on narrow screens */
|
|
2584
|
+
gap: calc(var(--exercise-keypad-gap) * 0.5);
|
|
2585
|
+
}
|
|
2319
2586
|
}
|
|
2320
2587
|
|
|
2321
2588
|
/* ========================================
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mathscan/math-exercise-engine",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.17",
|
|
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",
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
"scripts": {
|
|
20
20
|
"build": "vite build",
|
|
21
21
|
"dev": "vite build --watch",
|
|
22
|
+
"playground": "vite --open /playground/grid-symmetry-builder/",
|
|
22
23
|
"demo": "node examples/demo.mjs",
|
|
23
24
|
"demo:ts": "tsx --conditions=import examples/demo.ts",
|
|
24
25
|
"demo:build": "pnpm run build && node examples/demo.mjs",
|