@mathscan/math-exercise-engine 1.0.10 → 1.0.12

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
@@ -100,6 +100,17 @@
100
100
 
101
101
  /** Border width for inputs and buttons */
102
102
  --exercise-border-width: 2px;
103
+
104
+ /* ========================================
105
+ NUMBER LINE — HIGHLIGHT COLORS
106
+ ======================================== */
107
+
108
+ --nl-highlight-blue: rgba(0, 122, 204, 0.25);
109
+ --nl-highlight-red: rgba(239, 68, 68, 0.25);
110
+ --nl-highlight-green: rgba(34, 197, 94, 0.25);
111
+ --nl-highlight-orange: rgba(249, 115, 22, 0.25);
112
+ --nl-highlight-purple: rgba(112, 93, 192, 0.25);
113
+ --nl-highlight-yellow: rgba(255, 215, 0, 0.35);
103
114
  }
104
115
 
105
116
  /* Layout & Structure */
@@ -574,6 +585,8 @@ td.exercise-table-cell, th.exercise-table-cell {
574
585
  margin-bottom: var(--exercise-margin-bottom);
575
586
  user-select: none;
576
587
  --nl-chip-zone-height: 3rem;
588
+ /* Visual gap between end ticks and arrowheads — pure CSS, no effect on calculations */
589
+ --nl-arrow-gap: 8px;
577
590
  }
578
591
 
579
592
  /* Chip zone: push the axis row down so chips overflowing upward
@@ -615,7 +628,7 @@ td.exercise-table-cell, th.exercise-table-cell {
615
628
 
616
629
  /* In drag mode, chip zone is handled by .number-line--drag-mode padding-top on root */
617
630
 
618
- /* Horizontal track line — spans [--nl-side-pad, 100% - --nl-side-pad] */
631
+ /* Horizontal track line — spans [--nl-side-pad, 100% - --nl-side-pad] by default */
619
632
 
620
633
  .number-line-track {
621
634
  position: absolute;
@@ -629,6 +642,25 @@ td.exercise-table-cell, th.exercise-table-cell {
629
642
  transition: all 0.2s ease;
630
643
  }
631
644
 
645
+ /* Extend the track toward the arrow only when the arrow is present —
646
+ keeps the track flush with the last tick when no arrow exists on that side */
647
+
648
+ .number-line-axis-row:has(.number-line-arrow-left) .number-line-track {
649
+ left: calc(var(--nl-side-pad, 7%) - var(--nl-arrow-gap, 8px));
650
+ }
651
+
652
+ .number-line-axis-row:has(.number-line-arrow-right) .number-line-track {
653
+ right: calc(var(--nl-side-pad, 7%) - var(--nl-arrow-gap, 8px));
654
+ }
655
+
656
+ /* Geometric line style — track extends to the full container edges
657
+ so the axis looks like an infinite line, not a closed segment */
658
+
659
+ .number-line--style-line .number-line-track {
660
+ left: 0;
661
+ right: 0;
662
+ }
663
+
632
664
  /* ── Arrowheads (CSS border-triangle technique) ── */
633
665
 
634
666
  .number-line-arrow {
@@ -638,26 +670,36 @@ td.exercise-table-cell, th.exercise-table-cell {
638
670
  height: 0;
639
671
  }
640
672
 
641
- /* Right-pointing arrow — base sits at the right end of the track */
673
+ /* Right-pointing arrow — base sits at the right end of the track, offset outward by --nl-arrow-gap */
642
674
 
643
675
  .number-line-arrow-right {
644
- left: calc(100% - var(--nl-side-pad, 7%));
676
+ left: calc(100% - var(--nl-side-pad, 7%) + var(--nl-arrow-gap, 8px));
645
677
  transform: translateY(-50%);
646
678
  border-top: 5px solid transparent;
647
679
  border-bottom: 5px solid transparent;
648
680
  border-left: 8px solid var(--exercise-text, #333);
649
681
  }
650
682
 
651
- /* Left-pointing arrow — tip extends just past the left end of the track */
683
+ /* Left-pointing arrow — tip extends just past the left end of the track, offset outward by --nl-arrow-gap */
652
684
 
653
685
  .number-line-arrow-left {
654
- left: var(--nl-side-pad, 7%);
686
+ left: calc(var(--nl-side-pad, 7%) - var(--nl-arrow-gap, 8px));
655
687
  transform: translate(-100%, -50%);
656
688
  border-top: 5px solid transparent;
657
689
  border-bottom: 5px solid transparent;
658
690
  border-right: 8px solid var(--exercise-text, #333);
659
691
  }
660
692
 
693
+ /* Geometric line style — arrows reach the full container edges */
694
+
695
+ .number-line--style-line .number-line-arrow-right {
696
+ left: calc(100%);
697
+ }
698
+
699
+ .number-line--style-line .number-line-arrow-left {
700
+ left: 0;
701
+ }
702
+
661
703
  /* ── Major ticks ── */
662
704
 
663
705
  .number-line-major-tick {
@@ -667,6 +709,7 @@ td.exercise-table-cell, th.exercise-table-cell {
667
709
  width: 2px;
668
710
  height: 14px;
669
711
  background-color: var(--exercise-text, #333);
712
+ transition: transform 0.1s ease, background-color 0.1s ease, width 0.1s ease;
670
713
  }
671
714
 
672
715
  /* ── Minor ticks ── */
@@ -678,6 +721,7 @@ td.exercise-table-cell, th.exercise-table-cell {
678
721
  width: 1px;
679
722
  height: 8px;
680
723
  background-color: var(--exercise-text-light, #555);
724
+ transition: transform 0.1s ease, background-color 0.1s ease, width 0.1s ease;
681
725
  }
682
726
 
683
727
  /* ── Position marker dots ── */
@@ -686,12 +730,11 @@ td.exercise-table-cell, th.exercise-table-cell {
686
730
  position: absolute;
687
731
  top: 50%;
688
732
  transform: translate(-50%, -50%);
689
- width: 16px;
690
- height: 16px;
733
+ width: 10px;
734
+ height: 10px;
691
735
  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);
736
+ background-color: var(--exercise-accent, #705DC0);
737
+ box-shadow: 0 1px 3px rgba(112, 93, 192, 0.25);
695
738
  z-index: 2;
696
739
  }
697
740
 
@@ -705,10 +748,20 @@ td.exercise-table-cell, th.exercise-table-cell {
705
748
  *
706
749
  * Structure:
707
750
  * .number-line-placed-chip — absolute wrapper, positioned by left%
708
- * .number-line-placed-chip-btn — the visible chip button
751
+ * .number-line-placed-chip-inner — flex row holding btn + X button
752
+ * .number-line-placed-chip-btn — the visible chip button (draggable)
753
+ * .number-line-placed-chip-remove — ✕ badge (always visible)
709
754
  * ::after pseudo — thin connector line down to the axis
710
755
  */
711
756
 
757
+ /* Drop landing animation — subtle scale for professional feel */
758
+
759
+ @keyframes nl-chip-land {
760
+ 0% { transform: translateX(-50%) scale(0.75); opacity: 0.6; }
761
+ 60% { transform: translateX(-50%) scale(1.06); opacity: 1; }
762
+ 100% { transform: translateX(-50%) scale(1); opacity: 1; }
763
+ }
764
+
712
765
  .number-line-placed-chip {
713
766
  position: absolute;
714
767
  bottom: calc(50% + 2px);
@@ -721,48 +774,134 @@ td.exercise-table-cell, th.exercise-table-cell {
721
774
  pointer-events: none;
722
775
  }
723
776
 
777
+ /* Play landing animation when class is toggled on by React */
778
+
779
+ .nl-chip-landing {
780
+ animation: nl-chip-land 0.28s ease-out forwards;
781
+ }
782
+
783
+ /* Hide original chip while DragOverlay clone follows cursor — no ghost double */
784
+
785
+ .nl-chip-dragging {
786
+ opacity: 0 !important;
787
+ pointer-events: none;
788
+ }
789
+
790
+ .nl-chip-dragging::after {
791
+ display: none;
792
+ }
793
+
724
794
  /* Thin connector between chip and axis */
725
795
 
726
796
  .number-line-placed-chip::after {
727
797
  content: '';
728
798
  display: block;
729
- width: 2px;
730
- height: 12px;
731
- background-color: var(--exercise-primary, #007acc);
732
- margin-top: 2px;
799
+ width: 1.5px;
800
+ height: 10px;
801
+ background-color: var(--exercise-accent, #705DC0);
802
+ opacity: 0.5;
803
+ margin-top: 1px;
804
+ }
805
+
806
+ /* Item 3 — Inner flex row: position: relative so ✕ badge can be anchored inside */
807
+
808
+ .number-line-placed-chip-inner {
809
+ position: relative;
810
+ display: inline-flex;
811
+ align-items: center;
812
+ pointer-events: all;
813
+ }
814
+
815
+ /* ✕ badge: hidden by default, revealed on chip hover — clean, non-distracting */
816
+
817
+ .number-line-placed-chip-remove {
818
+ position: absolute;
819
+ top: -6px;
820
+ right: -6px;
821
+ display: flex;
822
+ align-items: center;
823
+ justify-content: center;
824
+ width: 16px;
825
+ height: 16px;
826
+ border-radius: 50%;
827
+ border: 1.5px solid white;
828
+ background-color: var(--exercise-text-light, #555);
829
+ color: white;
830
+ font-size: 0.5rem;
831
+ font-weight: bold;
832
+ line-height: 1;
833
+ cursor: pointer;
834
+ transition: opacity 0.15s ease, transform 0.12s ease, background-color 0.12s ease;
835
+ pointer-events: all;
836
+ z-index: 1;
837
+ opacity: 0;
838
+ }
839
+
840
+ /* Show ✕ on chip hover or when badge itself has focus */
841
+
842
+ .number-line-placed-chip-inner:hover .number-line-placed-chip-remove,
843
+ .number-line-placed-chip-remove:focus-visible {
844
+ opacity: 1;
845
+ }
846
+
847
+ .number-line-placed-chip-remove:hover {
848
+ opacity: 1;
849
+ transform: scale(1.15);
850
+ background-color: var(--exercise-danger, #ef4444);
733
851
  }
734
852
 
735
853
  .number-line-placed-chip-btn {
736
854
  pointer-events: all;
737
- background-color: var(--exercise-primary, #007acc);
855
+ background-color: var(--exercise-accent, #705DC0);
738
856
  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);
857
+ border: 2px solid rgba(255, 255, 255, 0.2);
858
+ border-radius: 8px;
859
+ padding: 6px 14px;
743
860
  font-size: var(--exercise-font-size, 0.875rem);
744
861
  font-weight: var(--exercise-font-weight-bold, 600);
745
862
  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;
863
+ box-shadow: 0 2px 8px rgba(112, 93, 192, 0.25);
864
+ transition: all 0.15s ease;
749
865
  white-space: nowrap;
750
866
  }
751
867
 
752
868
  .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);
869
+ filter: brightness(1.08);
870
+ box-shadow: 0 3px 10px rgba(112, 93, 192, 0.35);
756
871
  }
757
872
 
758
- /* Axis highlight while a chip is hovering over it */
873
+ /* Nearest snap tick highlights during drag — scaleY keeps layout intact */
874
+
875
+ .number-line-major-tick.nl-tick-hover {
876
+ transform: translate(-50%, -50%) scaleY(1.5);
877
+ width: 2.5px;
878
+ background-color: var(--exercise-accent, #705DC0);
879
+ }
759
880
 
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);
881
+ .number-line-minor-tick.nl-tick-hover {
882
+ transform: translate(-50%, -50%) scaleY(1.8);
883
+ width: 1.5px;
884
+ background-color: var(--exercise-accent, #705DC0);
764
885
  }
765
886
 
887
+ /* Snap preview — static subtle dot at landing position */
888
+
889
+ .nl-snap-preview {
890
+ position: absolute;
891
+ top: 50%;
892
+ transform: translate(-50%, -50%);
893
+ width: 12px;
894
+ height: 12px;
895
+ border-radius: 50%;
896
+ background-color: var(--exercise-accent, #705DC0);
897
+ opacity: 0.3;
898
+ pointer-events: none;
899
+ z-index: 2;
900
+ transition: left 0.06s ease;
901
+ }
902
+
903
+ /* Axis glow on drag-over intentionally disabled — axis stays neutral */
904
+
766
905
  /* ========================================
767
906
  ROW 3 — NUMERIC TICK LABELS
768
907
  ======================================== */
@@ -783,6 +922,130 @@ td.exercise-table-cell, th.exercise-table-cell {
783
922
  white-space: nowrap;
784
923
  }
785
924
 
925
+ /* ========================================
926
+ HIGHLIGHT BANDS
927
+ ======================================== */
928
+
929
+ /* A coloured band spanning [from%, to%] on the axis row */
930
+
931
+ .number-line-highlight-band {
932
+ position: absolute;
933
+ top: 0;
934
+ height: 100%;
935
+ background-color: var(--nl-highlight-color, var(--nl-highlight-blue, rgba(0, 122, 204, 0.25)));
936
+ border-radius: 2px;
937
+ z-index: 0;
938
+ pointer-events: none;
939
+ }
940
+
941
+ /* Highlight label row — sits below tick labels, never overlaps inputs */
942
+
943
+ .number-line-highlight-labels-row {
944
+ position: relative;
945
+ width: 100%;
946
+ height: 1.2rem;
947
+ margin-top: 0.1rem;
948
+ }
949
+
950
+ .number-line-highlight-label {
951
+ position: absolute;
952
+ transform: translateX(-50%);
953
+ font-size: calc(var(--exercise-font-size) * 0.75);
954
+ font-weight: var(--exercise-font-weight-bold, 600);
955
+ color: var(--exercise-text-light, #555);
956
+ white-space: nowrap;
957
+ pointer-events: none;
958
+ }
959
+
960
+ /* ========================================
961
+ COUNTER UI
962
+ ======================================== */
963
+
964
+ .number-line-counter {
965
+ display: flex;
966
+ flex-direction: column;
967
+ align-items: center;
968
+ gap: 0.25rem;
969
+ margin-top: 0.5rem;
970
+ padding: 0.4rem 0.75rem;
971
+ width: fit-content;
972
+ margin-left: auto;
973
+ margin-right: auto;
974
+ background: var(--exercise-bg, white);
975
+ border: 1px solid var(--exercise-border, #ddd);
976
+ border-radius: 8px;
977
+ }
978
+
979
+ /* Inner row: − value + buttons as a cohesive stepper */
980
+
981
+ .number-line-counter-controls {
982
+ display: flex;
983
+ align-items: center;
984
+ justify-content: center;
985
+ gap: 0.5rem;
986
+ }
987
+
988
+ .number-line-counter-label {
989
+ font-size: calc(var(--exercise-font-size) * 0.85);
990
+ color: var(--exercise-text-light, #555);
991
+ text-align: center;
992
+ }
993
+
994
+ .number-line-counter-value {
995
+ font-size: calc(var(--exercise-font-size) * 1.15);
996
+ font-weight: 600;
997
+ font-variant-numeric: tabular-nums;
998
+ color: var(--exercise-text, #333);
999
+ min-width: 2ch;
1000
+ text-align: center;
1001
+ line-height: 1;
1002
+ }
1003
+
1004
+ .number-line-counter-btn {
1005
+ display: inline-flex;
1006
+ align-items: center;
1007
+ justify-content: center;
1008
+ min-width: 36px;
1009
+ min-height: 36px;
1010
+ width: 36px;
1011
+ height: 36px;
1012
+ border: 1.5px solid var(--exercise-accent, #705DC0);
1013
+ border-radius: 50%;
1014
+ background: transparent;
1015
+ color: var(--exercise-accent, #705DC0);
1016
+ font-size: 1.1rem;
1017
+ font-weight: 500;
1018
+ line-height: 1;
1019
+ cursor: pointer;
1020
+ transition: all 0.12s ease;
1021
+ -webkit-tap-highlight-color: transparent;
1022
+ }
1023
+
1024
+ .number-line-counter-btn:hover:not(:disabled) {
1025
+ background: var(--exercise-accent, #705DC0);
1026
+ color: white;
1027
+ }
1028
+
1029
+ .number-line-counter-btn:active:not(:disabled) {
1030
+ transform: scale(0.93);
1031
+ }
1032
+
1033
+ .number-line-counter-btn:disabled {
1034
+ opacity: 0.35;
1035
+ cursor: not-allowed;
1036
+ }
1037
+
1038
+ /* ========================================
1039
+ ACCESSIBILITY — FOCUS-VISIBLE RINGS (item 6)
1040
+ ======================================== */
1041
+
1042
+ .number-line-placed-chip-btn:focus-visible,
1043
+ .number-line-placed-chip-remove:focus-visible,
1044
+ .number-line-counter-btn:focus-visible {
1045
+ outline: 2px solid var(--exercise-accent, #705DC0);
1046
+ outline-offset: 2px;
1047
+ }
1048
+
786
1049
  /**
787
1050
  * Button & Action Styles
788
1051
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mathscan/math-exercise-engine",
3
- "version": "1.0.10",
3
+ "version": "1.0.12",
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",