@mathscan/math-exercise-engine 1.0.11 → 1.0.13

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
@@ -49,6 +49,8 @@
49
49
  --exercise-border: #ddd;
50
50
  --exercise-border-focus: var(--exercise-primary);
51
51
  --exercise-highlight-color: #aef59d;
52
+ --exercise-correct: #22c55e;
53
+ --exercise-incorrect: #ef4444;
52
54
 
53
55
  /* ========================================
54
56
  SPACING VARIABLES
@@ -100,6 +102,17 @@
100
102
 
101
103
  /** Border width for inputs and buttons */
102
104
  --exercise-border-width: 2px;
105
+
106
+ /* ========================================
107
+ NUMBER LINE — HIGHLIGHT COLORS
108
+ ======================================== */
109
+
110
+ --nl-highlight-blue: rgba(0, 122, 204, 0.25);
111
+ --nl-highlight-red: rgba(239, 68, 68, 0.25);
112
+ --nl-highlight-green: rgba(34, 197, 94, 0.25);
113
+ --nl-highlight-orange: rgba(249, 115, 22, 0.25);
114
+ --nl-highlight-purple: rgba(112, 93, 192, 0.25);
115
+ --nl-highlight-yellow: rgba(255, 215, 0, 0.35);
103
116
  }
104
117
 
105
118
  /* Layout & Structure */
@@ -170,7 +183,6 @@
170
183
  flex-wrap: wrap;
171
184
  align-items: center;
172
185
  justify-content: center;
173
- align-items: baseline;
174
186
  gap: var(--exercise-element-margin);
175
187
  padding-bottom: calc(var(--exercise-line-gap) * 0.5);
176
188
  }
@@ -360,6 +372,30 @@ mark {
360
372
  border-color: var(--exercise-border-focus);
361
373
  }
362
374
 
375
+ /* ========================================
376
+ VALIDATION STATUS
377
+ ======================================== */
378
+
379
+ .inline-input.widget--correct {
380
+ border-color: var(--exercise-correct, #22c55e);
381
+ box-shadow: 0 0 0 3px rgba(34, 197, 94, 0.15);
382
+ }
383
+
384
+ .inline-input.widget--incorrect {
385
+ border-color: var(--exercise-incorrect, #ef4444);
386
+ box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.15);
387
+ }
388
+
389
+ .inline-input-chip.widget--correct {
390
+ border-color: var(--exercise-correct, #22c55e);
391
+ box-shadow: 0 0 0 3px rgba(34, 197, 94, 0.15);
392
+ }
393
+
394
+ .inline-input-chip.widget--incorrect {
395
+ border-color: var(--exercise-incorrect, #ef4444);
396
+ box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.15);
397
+ }
398
+
363
399
  /**
364
400
  * Widget Styles
365
401
  *
@@ -468,6 +504,24 @@ button.drag-option {
468
504
  box-shadow: 0 2px 4px rgba(0, 122, 204, 0.2);
469
505
  }
470
506
 
507
+ /* ========================================
508
+ VALIDATION STATUS
509
+ ======================================== */
510
+
511
+ .radio-option.widget--correct {
512
+ background: var(--exercise-correct, #22c55e);
513
+ border-color: var(--exercise-correct, #22c55e);
514
+ color: white;
515
+ box-shadow: 0 2px 4px rgba(34, 197, 94, 0.25);
516
+ }
517
+
518
+ .radio-option.widget--incorrect {
519
+ background: var(--exercise-incorrect, #ef4444);
520
+ border-color: var(--exercise-incorrect, #ef4444);
521
+ color: white;
522
+ box-shadow: 0 2px 4px rgba(239, 68, 68, 0.25);
523
+ }
524
+
471
525
  /* ========================================
472
526
  NUMERIC KEYPAD
473
527
  ======================================== */
@@ -556,6 +610,64 @@ td.exercise-table-cell, th.exercise-table-cell {
556
610
  border: 2px solid #7c6cf2;
557
611
  }
558
612
 
613
+ /* ========================================
614
+ FRACTION WIDGET
615
+ ======================================== */
616
+
617
+ .fraction {
618
+ display: inline-flex;
619
+ flex-direction: column;
620
+ align-items: center;
621
+ vertical-align: -0.15em;
622
+ margin: 0 0.2em;
623
+ line-height: 1;
624
+ }
625
+
626
+ .fraction-numerator {
627
+ margin-bottom: 3px;
628
+ }
629
+
630
+ .fraction-numerator,
631
+ .fraction-denominator {
632
+ display: flex;
633
+ align-items: center;
634
+ justify-content: center;
635
+ min-width: 1.5em;
636
+ padding: 0.05em 0.1em;
637
+ line-height: 2.2;
638
+ }
639
+
640
+ .fraction-bar {
641
+ width: 100%;
642
+ min-width: 2em;
643
+ height: 3px;
644
+ background-color: var(--exercise-accent);
645
+ border-radius: 1px;
646
+ margin: 1px 0;
647
+ }
648
+
649
+ .fraction-fixed {
650
+ color: var(--exercise-text);
651
+ min-width: 1ch;
652
+ text-align: center;
653
+ }
654
+
655
+ .fraction-input {
656
+ width: 3.2em !important;
657
+ padding: 0.15em 0.2em;
658
+ font-size: 0.9em;
659
+ text-align: center;
660
+ }
661
+
662
+ .fraction-chip {
663
+ font-size: 0.9em;
664
+ padding: 0.1em 0.3em;
665
+ }
666
+
667
+ .fraction-slot-droppable {
668
+ display: inline-flex;
669
+ }
670
+
559
671
  /**
560
672
  * Number Line Styles
561
673
  *
@@ -574,6 +686,8 @@ td.exercise-table-cell, th.exercise-table-cell {
574
686
  margin-bottom: var(--exercise-margin-bottom);
575
687
  user-select: none;
576
688
  --nl-chip-zone-height: 3rem;
689
+ /* Visual gap between end ticks and arrowheads — pure CSS, no effect on calculations */
690
+ --nl-arrow-gap: 8px;
577
691
  }
578
692
 
579
693
  /* Chip zone: push the axis row down so chips overflowing upward
@@ -615,7 +729,7 @@ td.exercise-table-cell, th.exercise-table-cell {
615
729
 
616
730
  /* In drag mode, chip zone is handled by .number-line--drag-mode padding-top on root */
617
731
 
618
- /* Horizontal track line — spans [--nl-side-pad, 100% - --nl-side-pad] */
732
+ /* Horizontal track line — spans [--nl-side-pad, 100% - --nl-side-pad] by default */
619
733
 
620
734
  .number-line-track {
621
735
  position: absolute;
@@ -629,6 +743,25 @@ td.exercise-table-cell, th.exercise-table-cell {
629
743
  transition: all 0.2s ease;
630
744
  }
631
745
 
746
+ /* Extend the track toward the arrow only when the arrow is present —
747
+ keeps the track flush with the last tick when no arrow exists on that side */
748
+
749
+ .number-line-axis-row:has(.number-line-arrow-left) .number-line-track {
750
+ left: calc(var(--nl-side-pad, 7%) - var(--nl-arrow-gap, 8px));
751
+ }
752
+
753
+ .number-line-axis-row:has(.number-line-arrow-right) .number-line-track {
754
+ right: calc(var(--nl-side-pad, 7%) - var(--nl-arrow-gap, 8px));
755
+ }
756
+
757
+ /* Geometric line style — track extends to the full container edges
758
+ so the axis looks like an infinite line, not a closed segment */
759
+
760
+ .number-line--style-line .number-line-track {
761
+ left: 0;
762
+ right: 0;
763
+ }
764
+
632
765
  /* ── Arrowheads (CSS border-triangle technique) ── */
633
766
 
634
767
  .number-line-arrow {
@@ -638,26 +771,36 @@ td.exercise-table-cell, th.exercise-table-cell {
638
771
  height: 0;
639
772
  }
640
773
 
641
- /* Right-pointing arrow — base sits at the right end of the track */
774
+ /* Right-pointing arrow — base sits at the right end of the track, offset outward by --nl-arrow-gap */
642
775
 
643
776
  .number-line-arrow-right {
644
- left: calc(100% - var(--nl-side-pad, 7%));
777
+ left: calc(100% - var(--nl-side-pad, 7%) + var(--nl-arrow-gap, 8px));
645
778
  transform: translateY(-50%);
646
779
  border-top: 5px solid transparent;
647
780
  border-bottom: 5px solid transparent;
648
781
  border-left: 8px solid var(--exercise-text, #333);
649
782
  }
650
783
 
651
- /* Left-pointing arrow — tip extends just past the left end of the track */
784
+ /* Left-pointing arrow — tip extends just past the left end of the track, offset outward by --nl-arrow-gap */
652
785
 
653
786
  .number-line-arrow-left {
654
- left: var(--nl-side-pad, 7%);
787
+ left: calc(var(--nl-side-pad, 7%) - var(--nl-arrow-gap, 8px));
655
788
  transform: translate(-100%, -50%);
656
789
  border-top: 5px solid transparent;
657
790
  border-bottom: 5px solid transparent;
658
791
  border-right: 8px solid var(--exercise-text, #333);
659
792
  }
660
793
 
794
+ /* Geometric line style — arrows reach the full container edges */
795
+
796
+ .number-line--style-line .number-line-arrow-right {
797
+ left: calc(100%);
798
+ }
799
+
800
+ .number-line--style-line .number-line-arrow-left {
801
+ left: 0;
802
+ }
803
+
661
804
  /* ── Major ticks ── */
662
805
 
663
806
  .number-line-major-tick {
@@ -667,6 +810,7 @@ td.exercise-table-cell, th.exercise-table-cell {
667
810
  width: 2px;
668
811
  height: 14px;
669
812
  background-color: var(--exercise-text, #333);
813
+ transition: transform 0.1s ease, background-color 0.1s ease, width 0.1s ease;
670
814
  }
671
815
 
672
816
  /* ── Minor ticks ── */
@@ -678,6 +822,7 @@ td.exercise-table-cell, th.exercise-table-cell {
678
822
  width: 1px;
679
823
  height: 8px;
680
824
  background-color: var(--exercise-text-light, #555);
825
+ transition: transform 0.1s ease, background-color 0.1s ease, width 0.1s ease;
681
826
  }
682
827
 
683
828
  /* ── Position marker dots ── */
@@ -686,12 +831,11 @@ td.exercise-table-cell, th.exercise-table-cell {
686
831
  position: absolute;
687
832
  top: 50%;
688
833
  transform: translate(-50%, -50%);
689
- width: 16px;
690
- height: 16px;
834
+ width: 10px;
835
+ height: 10px;
691
836
  border-radius: 50%;
692
- background-color: var(--exercise-primary, #007acc);
693
- /** Shadow: same 0.2 opacity convention as .drag-option.selected */
694
- box-shadow: 0 2px 4px rgba(0, 122, 204, 0.2);
837
+ background-color: var(--exercise-accent, #705DC0);
838
+ box-shadow: 0 1px 3px rgba(112, 93, 192, 0.25);
695
839
  z-index: 2;
696
840
  }
697
841
 
@@ -705,10 +849,20 @@ td.exercise-table-cell, th.exercise-table-cell {
705
849
  *
706
850
  * Structure:
707
851
  * .number-line-placed-chip — absolute wrapper, positioned by left%
708
- * .number-line-placed-chip-btn — the visible chip button
852
+ * .number-line-placed-chip-inner — flex row holding btn + X button
853
+ * .number-line-placed-chip-btn — the visible chip button (draggable)
854
+ * .number-line-placed-chip-remove — ✕ badge (always visible)
709
855
  * ::after pseudo — thin connector line down to the axis
710
856
  */
711
857
 
858
+ /* Drop landing animation — subtle scale for professional feel */
859
+
860
+ @keyframes nl-chip-land {
861
+ 0% { transform: translateX(-50%) scale(0.75); opacity: 0.6; }
862
+ 60% { transform: translateX(-50%) scale(1.06); opacity: 1; }
863
+ 100% { transform: translateX(-50%) scale(1); opacity: 1; }
864
+ }
865
+
712
866
  .number-line-placed-chip {
713
867
  position: absolute;
714
868
  bottom: calc(50% + 2px);
@@ -721,48 +875,134 @@ td.exercise-table-cell, th.exercise-table-cell {
721
875
  pointer-events: none;
722
876
  }
723
877
 
878
+ /* Play landing animation when class is toggled on by React */
879
+
880
+ .nl-chip-landing {
881
+ animation: nl-chip-land 0.28s ease-out forwards;
882
+ }
883
+
884
+ /* Hide original chip while DragOverlay clone follows cursor — no ghost double */
885
+
886
+ .nl-chip-dragging {
887
+ opacity: 0 !important;
888
+ pointer-events: none;
889
+ }
890
+
891
+ .nl-chip-dragging::after {
892
+ display: none;
893
+ }
894
+
724
895
  /* Thin connector between chip and axis */
725
896
 
726
897
  .number-line-placed-chip::after {
727
898
  content: '';
728
899
  display: block;
729
- width: 2px;
730
- height: 12px;
731
- background-color: var(--exercise-primary, #007acc);
732
- margin-top: 2px;
900
+ width: 1.5px;
901
+ height: 10px;
902
+ background-color: var(--exercise-accent, #705DC0);
903
+ opacity: 0.5;
904
+ margin-top: 1px;
905
+ }
906
+
907
+ /* Item 3 — Inner flex row: position: relative so ✕ badge can be anchored inside */
908
+
909
+ .number-line-placed-chip-inner {
910
+ position: relative;
911
+ display: inline-flex;
912
+ align-items: center;
913
+ pointer-events: all;
914
+ }
915
+
916
+ /* ✕ badge: hidden by default, revealed on chip hover — clean, non-distracting */
917
+
918
+ .number-line-placed-chip-remove {
919
+ position: absolute;
920
+ top: -6px;
921
+ right: -6px;
922
+ display: flex;
923
+ align-items: center;
924
+ justify-content: center;
925
+ width: 16px;
926
+ height: 16px;
927
+ border-radius: 50%;
928
+ border: 1.5px solid white;
929
+ background-color: var(--exercise-text-light, #555);
930
+ color: white;
931
+ font-size: 0.5rem;
932
+ font-weight: bold;
933
+ line-height: 1;
934
+ cursor: pointer;
935
+ transition: opacity 0.15s ease, transform 0.12s ease, background-color 0.12s ease;
936
+ pointer-events: all;
937
+ z-index: 1;
938
+ opacity: 0;
939
+ }
940
+
941
+ /* Show ✕ on chip hover or when badge itself has focus */
942
+
943
+ .number-line-placed-chip-inner:hover .number-line-placed-chip-remove,
944
+ .number-line-placed-chip-remove:focus-visible {
945
+ opacity: 1;
946
+ }
947
+
948
+ .number-line-placed-chip-remove:hover {
949
+ opacity: 1;
950
+ transform: scale(1.15);
951
+ background-color: var(--exercise-danger, #ef4444);
733
952
  }
734
953
 
735
954
  .number-line-placed-chip-btn {
736
955
  pointer-events: all;
737
- background-color: var(--exercise-primary, #007acc);
956
+ background-color: var(--exercise-accent, #705DC0);
738
957
  color: white;
739
- /** White inner border uses border-width token for consistency */
740
- border: var(--exercise-border-width) solid white;
741
- border-radius: var(--exercise-border-radius, 0.25rem);
742
- padding: var(--exercise-input-padding, 0.375rem 0.625rem);
958
+ border: 2px solid rgba(255, 255, 255, 0.2);
959
+ border-radius: 8px;
960
+ padding: 6px 14px;
743
961
  font-size: var(--exercise-font-size, 0.875rem);
744
962
  font-weight: var(--exercise-font-weight-bold, 600);
745
963
  cursor: pointer;
746
- /** Shadow: same convention as .drag-option.selected and .radio-option.selected */
747
- box-shadow: 0 2px 4px rgba(0, 122, 204, 0.2);
748
- transition: all 0.2s ease;
964
+ box-shadow: 0 2px 8px rgba(112, 93, 192, 0.25);
965
+ transition: all 0.15s ease;
749
966
  white-space: nowrap;
750
967
  }
751
968
 
752
969
  .number-line-placed-chip-btn:hover {
753
- background-color: var(--exercise-danger, #ef4444);
754
- /** Scale down like other interactive elements (consistent with .drag-option:active) */
755
- transform: scale(0.98);
970
+ filter: brightness(1.08);
971
+ box-shadow: 0 3px 10px rgba(112, 93, 192, 0.35);
972
+ }
973
+
974
+ /* Nearest snap tick highlights during drag — scaleY keeps layout intact */
975
+
976
+ .number-line-major-tick.nl-tick-hover {
977
+ transform: translate(-50%, -50%) scaleY(1.5);
978
+ width: 2.5px;
979
+ background-color: var(--exercise-accent, #705DC0);
980
+ }
981
+
982
+ .number-line-minor-tick.nl-tick-hover {
983
+ transform: translate(-50%, -50%) scaleY(1.8);
984
+ width: 1.5px;
985
+ background-color: var(--exercise-accent, #705DC0);
756
986
  }
757
987
 
758
- /* Axis highlight while a chip is hovering over it */
988
+ /* Snap preview — static subtle dot at landing position */
759
989
 
760
- .nl-axis-drag-over .number-line-track {
761
- background-color: var(--exercise-primary, #007acc);
762
- /** Shadow: same 0.2 opacity convention */
763
- box-shadow: 0 0 0 3px rgba(0, 122, 204, 0.2);
990
+ .nl-snap-preview {
991
+ position: absolute;
992
+ top: 50%;
993
+ transform: translate(-50%, -50%);
994
+ width: 12px;
995
+ height: 12px;
996
+ border-radius: 50%;
997
+ background-color: var(--exercise-accent, #705DC0);
998
+ opacity: 0.3;
999
+ pointer-events: none;
1000
+ z-index: 2;
1001
+ transition: left 0.06s ease;
764
1002
  }
765
1003
 
1004
+ /* Axis glow on drag-over intentionally disabled — axis stays neutral */
1005
+
766
1006
  /* ========================================
767
1007
  ROW 3 — NUMERIC TICK LABELS
768
1008
  ======================================== */
@@ -783,6 +1023,130 @@ td.exercise-table-cell, th.exercise-table-cell {
783
1023
  white-space: nowrap;
784
1024
  }
785
1025
 
1026
+ /* ========================================
1027
+ HIGHLIGHT BANDS
1028
+ ======================================== */
1029
+
1030
+ /* A coloured band spanning [from%, to%] on the axis row */
1031
+
1032
+ .number-line-highlight-band {
1033
+ position: absolute;
1034
+ top: 0;
1035
+ height: 100%;
1036
+ background-color: var(--nl-highlight-color, var(--nl-highlight-blue, rgba(0, 122, 204, 0.25)));
1037
+ border-radius: 2px;
1038
+ z-index: 0;
1039
+ pointer-events: none;
1040
+ }
1041
+
1042
+ /* Highlight label row — sits below tick labels, never overlaps inputs */
1043
+
1044
+ .number-line-highlight-labels-row {
1045
+ position: relative;
1046
+ width: 100%;
1047
+ height: 1.2rem;
1048
+ margin-top: 0.1rem;
1049
+ }
1050
+
1051
+ .number-line-highlight-label {
1052
+ position: absolute;
1053
+ transform: translateX(-50%);
1054
+ font-size: calc(var(--exercise-font-size) * 0.75);
1055
+ font-weight: var(--exercise-font-weight-bold, 600);
1056
+ color: var(--exercise-text-light, #555);
1057
+ white-space: nowrap;
1058
+ pointer-events: none;
1059
+ }
1060
+
1061
+ /* ========================================
1062
+ COUNTER UI
1063
+ ======================================== */
1064
+
1065
+ .number-line-counter {
1066
+ display: flex;
1067
+ flex-direction: column;
1068
+ align-items: center;
1069
+ gap: 0.25rem;
1070
+ margin-top: 0.5rem;
1071
+ padding: 0.4rem 0.75rem;
1072
+ width: fit-content;
1073
+ margin-left: auto;
1074
+ margin-right: auto;
1075
+ background: var(--exercise-bg, white);
1076
+ border: 1px solid var(--exercise-border, #ddd);
1077
+ border-radius: 8px;
1078
+ }
1079
+
1080
+ /* Inner row: − value + buttons as a cohesive stepper */
1081
+
1082
+ .number-line-counter-controls {
1083
+ display: flex;
1084
+ align-items: center;
1085
+ justify-content: center;
1086
+ gap: 0.5rem;
1087
+ }
1088
+
1089
+ .number-line-counter-label {
1090
+ font-size: calc(var(--exercise-font-size) * 0.85);
1091
+ color: var(--exercise-text-light, #555);
1092
+ text-align: center;
1093
+ }
1094
+
1095
+ .number-line-counter-value {
1096
+ font-size: calc(var(--exercise-font-size) * 1.15);
1097
+ font-weight: 600;
1098
+ font-variant-numeric: tabular-nums;
1099
+ color: var(--exercise-text, #333);
1100
+ min-width: 2ch;
1101
+ text-align: center;
1102
+ line-height: 1;
1103
+ }
1104
+
1105
+ .number-line-counter-btn {
1106
+ display: inline-flex;
1107
+ align-items: center;
1108
+ justify-content: center;
1109
+ min-width: 36px;
1110
+ min-height: 36px;
1111
+ width: 36px;
1112
+ height: 36px;
1113
+ border: 1.5px solid var(--exercise-accent, #705DC0);
1114
+ border-radius: 50%;
1115
+ background: transparent;
1116
+ color: var(--exercise-accent, #705DC0);
1117
+ font-size: 1.1rem;
1118
+ font-weight: 500;
1119
+ line-height: 1;
1120
+ cursor: pointer;
1121
+ transition: all 0.12s ease;
1122
+ -webkit-tap-highlight-color: transparent;
1123
+ }
1124
+
1125
+ .number-line-counter-btn:hover:not(:disabled) {
1126
+ background: var(--exercise-accent, #705DC0);
1127
+ color: white;
1128
+ }
1129
+
1130
+ .number-line-counter-btn:active:not(:disabled) {
1131
+ transform: scale(0.93);
1132
+ }
1133
+
1134
+ .number-line-counter-btn:disabled {
1135
+ opacity: 0.35;
1136
+ cursor: not-allowed;
1137
+ }
1138
+
1139
+ /* ========================================
1140
+ ACCESSIBILITY — FOCUS-VISIBLE RINGS (item 6)
1141
+ ======================================== */
1142
+
1143
+ .number-line-placed-chip-btn:focus-visible,
1144
+ .number-line-placed-chip-remove:focus-visible,
1145
+ .number-line-counter-btn:focus-visible {
1146
+ outline: 2px solid var(--exercise-accent, #705DC0);
1147
+ outline-offset: 2px;
1148
+ }
1149
+
786
1150
  /**
787
1151
  * Button & Action Styles
788
1152
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mathscan/math-exercise-engine",
3
- "version": "1.0.11",
3
+ "version": "1.0.13",
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",