@mathscan/math-exercise-engine 1.1.14 → 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/index.js CHANGED
@@ -231,6 +231,9 @@ const SIDE_PAD = 7;
231
231
  const CSS_VAR_SIDE_PAD = "--nl-side-pad";
232
232
  const NUMBER_LINE_LABELS = ["all", "extremes", "min-only", "max-only", "none"];
233
233
  const NUMBER_LINE_HIGHLIGHT_COLORS = ["blue", "red", "green", "orange", "purple", "yellow"];
234
+ const HEX_COLOR_REGEX = /^#([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6})$/;
235
+ const isNumberLineHighlightColor = (value) => NUMBER_LINE_HIGHLIGHT_COLORS.includes(value) || HEX_COLOR_REGEX.test(value);
236
+ const highlightColorMessage = (block) => `${block}: color must be one of ${NUMBER_LINE_HIGHLIGHT_COLORS.join(", ")} or a hex value (e.g. "#3B82F6")`;
234
237
  const NUMBER_LINE_DRAG_MODES = ["axis", "inputs"];
235
238
  function getFractionKey(widgetId, part) {
236
239
  return `${widgetId}__${part}`;
@@ -750,6 +753,11 @@ class MathExerciseParser extends chevrotain.CstParser {
750
753
  this.OR([{ ALT: () => this.CONSUME(Space) }, { ALT: () => this.CONSUME(Newline) }]);
751
754
  });
752
755
  this.CONSUME(ROW);
756
+ this.OPTION(() => {
757
+ this.CONSUME(LBracket);
758
+ this.SUBRULE(this.parameterList);
759
+ this.CONSUME(RBracket);
760
+ });
753
761
  this.MANY2(() => {
754
762
  this.OR2([{ ALT: () => this.CONSUME2(Space) }, { ALT: () => this.CONSUME2(Newline) }]);
755
763
  });
@@ -1596,7 +1604,9 @@ const ImageInlineSchema = zod.z.object({
1596
1604
  }),
1597
1605
  alt: zod.z.string(),
1598
1606
  width: zod.z.number().positive().optional(),
1599
- height: zod.z.number().positive().optional()
1607
+ height: zod.z.number().positive().optional(),
1608
+ /** Opt-in: show an expand control that opens the image fullscreen. */
1609
+ fullscreen: zod.z.boolean().optional()
1600
1610
  });
1601
1611
  const VisualFractionInlineSchema = zod.z.object({
1602
1612
  type: zod.z.literal(AST_TYPES.VISUAL_FRACTION),
@@ -1901,10 +1911,8 @@ const NumberLineHighlightSchema = zod.z.object({
1901
1911
  type: zod.z.literal(AST_TYPES.NUMBER_LINE_HIGHLIGHT),
1902
1912
  from: zod.z.number().finite({ message: "HIGHLIGHT: from must be a finite number" }),
1903
1913
  to: zod.z.number().finite({ message: "HIGHLIGHT: to must be a finite number" }),
1904
- color: zod.z.enum(NUMBER_LINE_HIGHLIGHT_COLORS, {
1905
- errorMap: () => ({
1906
- message: `HIGHLIGHT: color must be one of ${NUMBER_LINE_HIGHLIGHT_COLORS.join(", ")}`
1907
- })
1914
+ color: zod.z.string().refine(isNumberLineHighlightColor, {
1915
+ message: highlightColorMessage("HIGHLIGHT")
1908
1916
  }).optional(),
1909
1917
  label: zod.z.string().optional()
1910
1918
  }).refine((d) => d.from < d.to, {
@@ -1923,7 +1931,7 @@ const NumberLineCursorSchema = zod.z.object({
1923
1931
  type: zod.z.literal(AST_TYPES.NUMBER_LINE_CURSOR),
1924
1932
  value: zod.z.number().finite({ message: "CURSOR: value must be a finite number" }).default(0),
1925
1933
  answer: zod.z.number({ required_error: "CURSOR: answer is required" }).finite({ message: "CURSOR: answer must be a finite number" }),
1926
- color: zod.z.string().regex(/^#([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6})$/, {
1934
+ color: zod.z.string().regex(HEX_COLOR_REGEX, {
1927
1935
  message: 'CURSOR: color must be a hex value (e.g. "#3B82F6" or "#F00")'
1928
1936
  }).optional(),
1929
1937
  fillRange: zod.z.boolean().default(false),
@@ -1945,8 +1953,8 @@ const NumberLineArrowSchema = zod.z.object({
1945
1953
  to: zod.z.number().finite({ message: "ARROW: to must be a finite number" }),
1946
1954
  color: zod.z.string().optional(),
1947
1955
  inlines: zod.z.array(InlineNodeSchema)
1948
- }).refine((d) => d.from < d.to, {
1949
- message: "ARROW: from must be less than to"
1956
+ }).refine((d) => d.from !== d.to, {
1957
+ message: "ARROW: from and to must be different"
1950
1958
  });
1951
1959
  function createNumberLineArrow(from, to, inlines, color) {
1952
1960
  return NumberLineArrowSchema.parse({
@@ -2011,7 +2019,9 @@ const NumberLineBlockSchema = zod.z.object({
2011
2019
  }
2012
2020
  }
2013
2021
  for (const arrow of data.arrows ?? []) {
2014
- if (arrow.from < data.min || arrow.to > data.max) {
2022
+ const lo = Math.min(arrow.from, arrow.to);
2023
+ const hi = Math.max(arrow.from, arrow.to);
2024
+ if (lo < data.min || hi > data.max) {
2015
2025
  ctx.addIssue({
2016
2026
  code: zod.z.ZodIssueCode.custom,
2017
2027
  message: `NUMBER_LINE: ARROW range [${arrow.from}, ${arrow.to}] must be within [${data.min}, ${data.max}]`
@@ -2062,23 +2072,27 @@ const TableCellSchema = zod.z.object({
2062
2072
  type: zod.z.literal(AST_TYPES.TABLE_CELL),
2063
2073
  colspan: zod.z.number().int().positive().optional(),
2064
2074
  rowspan: zod.z.number().int().positive().optional(),
2075
+ backgroundColor: zod.z.string().optional(),
2065
2076
  inlines: zod.z.array(InlineNodeSchema)
2066
2077
  });
2067
- function createTableCell(inlines, colspan, rowspan) {
2078
+ function createTableCell(inlines, colspan, rowspan, backgroundColor) {
2068
2079
  return TableCellSchema.parse({
2069
2080
  type: AST_TYPES.TABLE_CELL,
2070
2081
  colspan,
2071
2082
  rowspan,
2083
+ backgroundColor,
2072
2084
  inlines
2073
2085
  });
2074
2086
  }
2075
2087
  const TableRowSchema = zod.z.object({
2076
2088
  type: zod.z.literal(AST_TYPES.TABLE_ROW),
2089
+ backgroundColor: zod.z.string().optional(),
2077
2090
  cells: zod.z.array(TableCellSchema).min(1)
2078
2091
  });
2079
- function createTableRow(cells) {
2092
+ function createTableRow(cells, backgroundColor) {
2080
2093
  return TableRowSchema.parse({
2081
2094
  type: AST_TYPES.TABLE_ROW,
2095
+ backgroundColor,
2082
2096
  cells
2083
2097
  });
2084
2098
  }
@@ -2678,15 +2692,18 @@ class NaturalDSLVisitor extends BaseCstVisitor {
2678
2692
  return createTableBlock(cols, rows);
2679
2693
  }
2680
2694
  row(ctx) {
2695
+ const params = ctx.parameterList ? this.visit(ctx.parameterList) : {};
2696
+ const backgroundColor = params.bgcolor;
2681
2697
  const cells = ctx.cell.map((c) => this.visit(c));
2682
- return createTableRow(cells);
2698
+ return createTableRow(cells, backgroundColor);
2683
2699
  }
2684
2700
  cell(ctx) {
2685
2701
  const params = ctx.parameterList ? this.visit(ctx.parameterList) : {};
2686
2702
  const colspan = params.colspan;
2687
2703
  const rowspan = params.rowspan;
2704
+ const backgroundColor = params.bgcolor;
2688
2705
  const inlines = ctx.inlineElement ? ctx.inlineElement.map((elem) => this.visit(elem)) : [];
2689
- return createTableCell(inlines, colspan, rowspan);
2706
+ return createTableCell(inlines, colspan, rowspan, backgroundColor);
2690
2707
  }
2691
2708
  // ═══════════════════════════════════════════════════════════════════════════
2692
2709
  // Number Line Block
@@ -3096,7 +3113,8 @@ class NaturalDSLVisitor extends BaseCstVisitor {
3096
3113
  const alt = params.alt;
3097
3114
  const width = params.width;
3098
3115
  const height = params.height;
3099
- return createImage(name, src, alt, { width, height });
3116
+ const fullscreen = params.fullscreen !== void 0 ? params.fullscreen === true || params.fullscreen === "true" : void 0;
3117
+ return createImage(name, src, alt, { width, height, fullscreen });
3100
3118
  }
3101
3119
  visualFraction(ctx) {
3102
3120
  const params = this.visit(ctx.parameterList);
@@ -3318,7 +3336,9 @@ const ImageItemSchema = zod.z.object({
3318
3336
  }),
3319
3337
  alt: zod.z.string(),
3320
3338
  width: zod.z.number().positive().optional(),
3321
- height: zod.z.number().positive().optional()
3339
+ height: zod.z.number().positive().optional(),
3340
+ /** Opt-in: show an expand control that opens the image fullscreen. */
3341
+ fullscreen: zod.z.boolean().optional()
3322
3342
  });
3323
3343
  const SpaceItemSchema = zod.z.object({
3324
3344
  id: zod.z.string(),
@@ -3388,11 +3408,13 @@ const TableCellItemSchema = zod.z.object({
3388
3408
  type: zod.z.literal(JSON_TYPES.TABLE_CELL),
3389
3409
  colspan: zod.z.number().int().positive().optional(),
3390
3410
  rowspan: zod.z.number().int().positive().optional(),
3411
+ backgroundColor: zod.z.string().optional(),
3391
3412
  items: zod.z.array(LayoutItemSchema)
3392
3413
  });
3393
3414
  const TableRowItemSchema = zod.z.object({
3394
3415
  id: zod.z.string(),
3395
3416
  type: zod.z.literal(JSON_TYPES.TABLE_ROW),
3417
+ backgroundColor: zod.z.string().optional(),
3396
3418
  cells: zod.z.array(TableCellItemSchema)
3397
3419
  });
3398
3420
  const TableItemSchema = zod.z.object({
@@ -3421,7 +3443,9 @@ const NumberLineHighlightItemSchema = zod.z.object({
3421
3443
  type: zod.z.literal(JSON_TYPES.NUMBER_LINE_HIGHLIGHT),
3422
3444
  from: zod.z.number().finite(),
3423
3445
  to: zod.z.number().finite(),
3424
- color: zod.z.enum(NUMBER_LINE_HIGHLIGHT_COLORS).optional(),
3446
+ color: zod.z.string().refine(isNumberLineHighlightColor, {
3447
+ message: highlightColorMessage("HIGHLIGHT")
3448
+ }).optional(),
3425
3449
  label: zod.z.string().optional()
3426
3450
  });
3427
3451
  const NumberLineCursorItemSchema = zod.z.object({
@@ -3429,7 +3453,7 @@ const NumberLineCursorItemSchema = zod.z.object({
3429
3453
  type: zod.z.literal(JSON_TYPES.NUMBER_LINE_CURSOR),
3430
3454
  value: zod.z.number().finite(),
3431
3455
  answer: zod.z.number().finite(),
3432
- color: zod.z.string().regex(/^#([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6})$/).optional(),
3456
+ color: zod.z.string().regex(HEX_COLOR_REGEX).optional(),
3433
3457
  fillRange: zod.z.boolean(),
3434
3458
  label: zod.z.string().optional()
3435
3459
  });
@@ -9677,13 +9701,20 @@ function transformTable(table, idGenerator, inlineDragIds) {
9677
9701
  if (cell.rowspan !== void 0) {
9678
9702
  cellItem.rowspan = cell.rowspan;
9679
9703
  }
9704
+ if (cell.backgroundColor !== void 0) {
9705
+ cellItem.backgroundColor = cell.backgroundColor;
9706
+ }
9680
9707
  return cellItem;
9681
9708
  });
9682
- return {
9709
+ const rowItem = {
9683
9710
  id: idGenerator.generate(JSON_TYPES.TABLE_ROW),
9684
9711
  type: JSON_TYPES.TABLE_ROW,
9685
9712
  cells
9686
9713
  };
9714
+ if (row.backgroundColor !== void 0) {
9715
+ rowItem.backgroundColor = row.backgroundColor;
9716
+ }
9717
+ return rowItem;
9687
9718
  });
9688
9719
  return {
9689
9720
  id: idGenerator.generate(JSON_TYPES.TABLE),
@@ -9768,7 +9799,8 @@ function transformInline(inline, idGenerator, inlineDragIds) {
9768
9799
  src: inline.src,
9769
9800
  alt: inline.alt,
9770
9801
  ...inline.width !== void 0 && { width: inline.width },
9771
- ...inline.height !== void 0 && { height: inline.height }
9802
+ ...inline.height !== void 0 && { height: inline.height },
9803
+ ...inline.fullscreen !== void 0 && { fullscreen: inline.fullscreen }
9772
9804
  };
9773
9805
  case AST_TYPES.VISUAL_FRACTION:
9774
9806
  return {
@@ -16638,6 +16670,226 @@ const WidgetRenderer = ({
16638
16670
  }
16639
16671
  return null;
16640
16672
  };
16673
+ const ExpandDiamondIcon = () => {
16674
+ const gradientId = React.useId();
16675
+ return /* @__PURE__ */ jsxRuntime.jsxs(
16676
+ "svg",
16677
+ {
16678
+ className: "image-zoom-icon",
16679
+ viewBox: "0 0 60 60",
16680
+ fill: "none",
16681
+ xmlns: "http://www.w3.org/2000/svg",
16682
+ "aria-hidden": "true",
16683
+ focusable: "false",
16684
+ children: [
16685
+ /* @__PURE__ */ jsxRuntime.jsx(
16686
+ "rect",
16687
+ {
16688
+ y: "-1.41531",
16689
+ width: "31.6754",
16690
+ height: "31.6754",
16691
+ rx: "7",
16692
+ transform: "matrix(0.706556 0.707657 -0.706556 0.707657 29.4526 10.0862)",
16693
+ fill: "white"
16694
+ }
16695
+ ),
16696
+ /* @__PURE__ */ jsxRuntime.jsx(
16697
+ "rect",
16698
+ {
16699
+ y: "-1.41531",
16700
+ width: "31.6754",
16701
+ height: "31.6754",
16702
+ rx: "7",
16703
+ transform: "matrix(0.706556 0.707657 -0.706556 0.707657 29.4526 10.0862)",
16704
+ stroke: "#BDAFFE",
16705
+ strokeWidth: "2"
16706
+ }
16707
+ ),
16708
+ /* @__PURE__ */ jsxRuntime.jsx(
16709
+ "rect",
16710
+ {
16711
+ y: "-1.41531",
16712
+ width: "31.6754",
16713
+ height: "31.6754",
16714
+ rx: "7",
16715
+ transform: "matrix(0.706556 0.707657 -0.706556 0.707657 29.4526 10.0862)",
16716
+ stroke: `url(#${gradientId})`,
16717
+ strokeOpacity: "0.3",
16718
+ strokeWidth: "2"
16719
+ }
16720
+ ),
16721
+ /* @__PURE__ */ jsxRuntime.jsxs(
16722
+ "g",
16723
+ {
16724
+ stroke: "currentColor",
16725
+ strokeWidth: "2.5",
16726
+ strokeLinecap: "round",
16727
+ strokeLinejoin: "round",
16728
+ children: [
16729
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M22.45 27 L22.45 23.5 L25.95 23.5" }),
16730
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M34.95 23.5 L38.45 23.5 L38.45 27" }),
16731
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M38.45 36 L38.45 39.5 L34.95 39.5" }),
16732
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M25.95 39.5 L22.45 39.5 L22.45 36" })
16733
+ ]
16734
+ }
16735
+ ),
16736
+ /* @__PURE__ */ jsxRuntime.jsx("defs", { children: /* @__PURE__ */ jsxRuntime.jsxs(
16737
+ "linearGradient",
16738
+ {
16739
+ id: gradientId,
16740
+ x1: "3.84406",
16741
+ y1: "3.97407",
16742
+ x2: "24.7079",
16743
+ y2: "24.8379",
16744
+ gradientUnits: "userSpaceOnUse",
16745
+ children: [
16746
+ /* @__PURE__ */ jsxRuntime.jsx("stop", { stopColor: "#FAFAFA" }),
16747
+ /* @__PURE__ */ jsxRuntime.jsx("stop", { offset: "1", stopColor: "#8A4899" })
16748
+ ]
16749
+ }
16750
+ ) })
16751
+ ]
16752
+ }
16753
+ );
16754
+ };
16755
+ const CloseIcon = () => /* @__PURE__ */ jsxRuntime.jsxs(
16756
+ "svg",
16757
+ {
16758
+ className: "image-lightbox-close-icon",
16759
+ viewBox: "0 0 24 24",
16760
+ fill: "none",
16761
+ xmlns: "http://www.w3.org/2000/svg",
16762
+ "aria-hidden": "true",
16763
+ focusable: "false",
16764
+ children: [
16765
+ /* @__PURE__ */ jsxRuntime.jsx(
16766
+ "path",
16767
+ {
16768
+ d: "M18 6L6 18",
16769
+ stroke: "currentColor",
16770
+ strokeWidth: "2",
16771
+ strokeLinecap: "round",
16772
+ strokeLinejoin: "round"
16773
+ }
16774
+ ),
16775
+ /* @__PURE__ */ jsxRuntime.jsx(
16776
+ "path",
16777
+ {
16778
+ d: "M6 6L18 18",
16779
+ stroke: "currentColor",
16780
+ strokeWidth: "2",
16781
+ strokeLinecap: "round",
16782
+ strokeLinejoin: "round"
16783
+ }
16784
+ )
16785
+ ]
16786
+ }
16787
+ );
16788
+ const ImageItem = ({
16789
+ src,
16790
+ alt,
16791
+ width,
16792
+ height,
16793
+ fullscreen = false
16794
+ }) => {
16795
+ const [isOpen, setIsOpen] = React.useState(false);
16796
+ const triggerRef = React.useRef(null);
16797
+ const closeRef = React.useRef(null);
16798
+ const open = React.useCallback(() => setIsOpen(true), []);
16799
+ const close = React.useCallback(() => setIsOpen(false), []);
16800
+ const isLightboxOpen = isOpen && fullscreen;
16801
+ React.useEffect(() => {
16802
+ if (!isLightboxOpen) return;
16803
+ const onKeyDown = (e) => {
16804
+ if (e.key === "Escape") {
16805
+ e.stopPropagation();
16806
+ close();
16807
+ }
16808
+ };
16809
+ document.addEventListener("keydown", onKeyDown);
16810
+ return () => document.removeEventListener("keydown", onKeyDown);
16811
+ }, [isLightboxOpen, close]);
16812
+ React.useEffect(() => {
16813
+ if (!isLightboxOpen) return;
16814
+ const previous = document.body.style.overflow;
16815
+ document.body.style.overflow = "hidden";
16816
+ return () => {
16817
+ document.body.style.overflow = previous;
16818
+ };
16819
+ }, [isLightboxOpen]);
16820
+ const wasOpen = React.useRef(false);
16821
+ React.useEffect(() => {
16822
+ if (isLightboxOpen) {
16823
+ closeRef.current?.focus();
16824
+ } else if (wasOpen.current) {
16825
+ triggerRef.current?.focus({ preventScroll: true });
16826
+ }
16827
+ wasOpen.current = isLightboxOpen;
16828
+ }, [isLightboxOpen]);
16829
+ const overlay = isLightboxOpen && typeof document !== "undefined" ? reactDom.createPortal(
16830
+ /* @__PURE__ */ jsxRuntime.jsxs(
16831
+ "div",
16832
+ {
16833
+ className: "image-lightbox",
16834
+ role: "dialog",
16835
+ "aria-modal": "true",
16836
+ "aria-label": alt || "Image",
16837
+ onClick: close,
16838
+ children: [
16839
+ /* @__PURE__ */ jsxRuntime.jsx(
16840
+ "button",
16841
+ {
16842
+ ref: closeRef,
16843
+ type: "button",
16844
+ className: "image-lightbox-close",
16845
+ "aria-label": "Close fullscreen image",
16846
+ onClick: close,
16847
+ children: /* @__PURE__ */ jsxRuntime.jsx(CloseIcon, {})
16848
+ }
16849
+ ),
16850
+ /* @__PURE__ */ jsxRuntime.jsx(
16851
+ "img",
16852
+ {
16853
+ className: "image-lightbox-img",
16854
+ src,
16855
+ alt,
16856
+ onClick: (e) => e.stopPropagation()
16857
+ }
16858
+ )
16859
+ ]
16860
+ }
16861
+ ),
16862
+ document.body
16863
+ ) : null;
16864
+ const img = /* @__PURE__ */ jsxRuntime.jsx(
16865
+ "img",
16866
+ {
16867
+ className: `image-item text-item ${fullscreen ? "image-item-zoomable" : ""}`,
16868
+ src,
16869
+ alt,
16870
+ ...width !== void 0 && { width },
16871
+ ...height !== void 0 && { height },
16872
+ loading: "lazy",
16873
+ ...fullscreen && { onClick: open }
16874
+ }
16875
+ );
16876
+ if (!fullscreen) return img;
16877
+ return /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "image-item-wrapper", children: [
16878
+ img,
16879
+ /* @__PURE__ */ jsxRuntime.jsx(
16880
+ "button",
16881
+ {
16882
+ ref: triggerRef,
16883
+ type: "button",
16884
+ className: "image-zoom-btn",
16885
+ "aria-label": alt ? `View "${alt}" fullscreen` : "View image fullscreen",
16886
+ onClick: open,
16887
+ children: /* @__PURE__ */ jsxRuntime.jsx(ExpandDiamondIcon, {})
16888
+ }
16889
+ ),
16890
+ overlay
16891
+ ] });
16892
+ };
16641
16893
  const EPSILON = 10 ** -INTERNAL_PRECISION;
16642
16894
  const cleanFloat = (n) => parseFloat(n.toFixed(INTERNAL_PRECISION));
16643
16895
  const roundTick = (n) => parseFloat(n.toFixed(DISPLAY_PRECISION));
@@ -16691,7 +16943,7 @@ const getPositionWidgetId = (content) => {
16691
16943
  }
16692
16944
  return null;
16693
16945
  };
16694
- const toHighlightColor = (color) => `var(--nl-highlight-${color})`;
16946
+ const toHighlightColor = (color) => color.startsWith("#") ? color : `var(--nl-highlight-${color})`;
16695
16947
  const getBandGeometry = (h, min, max) => {
16696
16948
  const leftPct = toPercent(Math.min(h.from, h.to), min, max);
16697
16949
  const rightPct = toPercent(Math.max(h.from, h.to), min, max);
@@ -17388,24 +17640,27 @@ function getArrowArcLabelTop(widthPct) {
17388
17640
  const arcTop = svgTop + ARROW_ARC_BASE_Y - getArrowArcLift(widthPct);
17389
17641
  return Math.max(0, arcTop - ARROW_ARC_LABEL_OFFSET);
17390
17642
  }
17391
- function getArrowArcPaths(widthPct, svgWidthPx = ARROW_ARC_VIEWBOX_WIDTH) {
17392
- const endX = getArrowArcSvgWidth(svgWidthPx);
17643
+ function getArrowArcPaths(widthPct, svgWidthPx = ARROW_ARC_VIEWBOX_WIDTH, reversed = false) {
17644
+ const width = getArrowArcSvgWidth(svgWidthPx);
17645
+ const startX = reversed ? width : 0;
17646
+ const tipX = reversed ? 0 : width;
17647
+ const sign = reversed ? -1 : 1;
17393
17648
  const controlInset = Math.min(svgWidthPx * 0.18, 48);
17394
17649
  const lift = getArrowArcLift(widthPct);
17395
17650
  const controlY = fmt(ARROW_ARC_BASE_Y - lift);
17396
- const tangentAngle = Math.atan2(ARROW_ARC_BASE_Y - controlY, endX - (endX - controlInset));
17397
- const tx = Math.cos(tangentAngle);
17651
+ const tangentAngle = Math.atan2(ARROW_ARC_BASE_Y - controlY, controlInset);
17652
+ const tx = Math.cos(tangentAngle) * sign;
17398
17653
  const ty = Math.sin(tangentAngle);
17399
- const shaftEndX = fmt(endX - tx * ARROW_ARC_HEAD_LENGTH);
17654
+ const shaftEndX = fmt(tipX - tx * ARROW_ARC_HEAD_LENGTH);
17400
17655
  const shaftEndY = fmt(ARROW_ARC_BASE_Y - ty * ARROW_ARC_HEAD_LENGTH);
17401
17656
  const endControlX = fmt(shaftEndX - tx * controlInset);
17402
17657
  const endControlY = fmt(shaftEndY - ty * controlInset);
17403
- const arcPath = `M 0 ${ARROW_ARC_BASE_Y} C ${fmt(controlInset)} ${controlY} ${endControlX} ${endControlY} ${shaftEndX} ${shaftEndY}`;
17658
+ const arcPath = `M ${fmt(startX)} ${ARROW_ARC_BASE_Y} C ${fmt(startX + sign * controlInset)} ${controlY} ${endControlX} ${endControlY} ${shaftEndX} ${shaftEndY}`;
17404
17659
  const nx = -ty;
17405
17660
  const ny = tx;
17406
- const baseX = endX - tx * ARROW_ARC_HEAD_LENGTH;
17661
+ const baseX = tipX - tx * ARROW_ARC_HEAD_LENGTH;
17407
17662
  const baseY = ARROW_ARC_BASE_Y - ty * ARROW_ARC_HEAD_LENGTH;
17408
- const headPath = `M ${fmt(endX)} ${ARROW_ARC_BASE_Y} L ${fmt(baseX + nx * ARROW_ARC_HEAD_HALF_WIDTH)} ${fmt(baseY + ny * ARROW_ARC_HEAD_HALF_WIDTH)} L ${fmt(baseX - nx * ARROW_ARC_HEAD_HALF_WIDTH)} ${fmt(baseY - ny * ARROW_ARC_HEAD_HALF_WIDTH)} Z`;
17663
+ const headPath = `M ${fmt(tipX)} ${ARROW_ARC_BASE_Y} L ${fmt(baseX + nx * ARROW_ARC_HEAD_HALF_WIDTH)} ${fmt(baseY + ny * ARROW_ARC_HEAD_HALF_WIDTH)} L ${fmt(baseX - nx * ARROW_ARC_HEAD_HALF_WIDTH)} ${fmt(baseY - ny * ARROW_ARC_HEAD_HALF_WIDTH)} Z`;
17409
17664
  return { arcPath, headPath };
17410
17665
  }
17411
17666
  function getArrowArcLayouts(arrows, min, max) {
@@ -17416,9 +17671,10 @@ function getArrowArcLayouts(arrows, min, max) {
17416
17671
  return {
17417
17672
  arrow,
17418
17673
  index,
17419
- leftPct: fromPct,
17420
- rightPct: toPct,
17421
- widthPct: toPct - fromPct,
17674
+ leftPct: Math.min(fromPct, toPct),
17675
+ rightPct: Math.max(fromPct, toPct),
17676
+ widthPct: Math.abs(toPct - fromPct),
17677
+ reversed: toPct < fromPct,
17422
17678
  level: 0
17423
17679
  };
17424
17680
  }).filter((layout) => layout.widthPct > 0).sort((a, b) => a.leftPct - b.leftPct || a.rightPct - b.rightPct || a.index - b.index);
@@ -17455,7 +17711,7 @@ function useElementWidth() {
17455
17711
  return [ref, width];
17456
17712
  }
17457
17713
  const ArrowArc = ({
17458
- layout: { arrow, leftPct, widthPct, level },
17714
+ layout: { arrow, leftPct, widthPct, reversed, level },
17459
17715
  widgets,
17460
17716
  values,
17461
17717
  droppedInputs,
@@ -17467,8 +17723,8 @@ const ArrowArc = ({
17467
17723
  const hasLabel = arrow.content.length > 0;
17468
17724
  const svgWidthPx = getArrowArcSvgWidth(renderedWidthPx);
17469
17725
  const { arcPath, headPath } = React.useMemo(
17470
- () => getArrowArcPaths(widthPct, svgWidthPx),
17471
- [widthPct, svgWidthPx]
17726
+ () => getArrowArcPaths(widthPct, svgWidthPx, reversed),
17727
+ [widthPct, svgWidthPx, reversed]
17472
17728
  );
17473
17729
  return /* @__PURE__ */ jsxRuntime.jsxs(
17474
17730
  "div",
@@ -18224,14 +18480,13 @@ const LayoutRenderer = ({
18224
18480
  }
18225
18481
  case JSON_TYPES.IMAGE:
18226
18482
  return /* @__PURE__ */ jsxRuntime.jsx(
18227
- "img",
18483
+ ImageItem,
18228
18484
  {
18229
- className: "image-item text-item",
18230
18485
  src: item.src,
18231
18486
  alt: item.alt,
18232
- ...item.width !== void 0 && { width: item.width },
18233
- ...item.height !== void 0 && { height: item.height },
18234
- loading: "lazy"
18487
+ width: item.width,
18488
+ height: item.height,
18489
+ fullscreen: item.fullscreen
18235
18490
  }
18236
18491
  );
18237
18492
  case JSON_TYPES.WIDGET_REF: {
@@ -18282,30 +18537,37 @@ const LayoutRenderer = ({
18282
18537
  },
18283
18538
  subItem.id
18284
18539
  )) });
18285
- case JSON_TYPES.TABLE:
18286
- return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "exercise-table-container", children: /* @__PURE__ */ jsxRuntime.jsx("table", { className: "exercise-table", children: /* @__PURE__ */ jsxRuntime.jsx("tbody", { children: item.rows.map((row) => /* @__PURE__ */ jsxRuntime.jsx("tr", { className: "exercise-table-row", children: row.cells.map((cell) => /* @__PURE__ */ jsxRuntime.jsx(
18287
- "td",
18288
- {
18289
- className: "exercise-table-cell",
18290
- colSpan: cell.colspan || 1,
18291
- rowSpan: cell.rowspan || 1,
18292
- children: cell.items.map((subItem) => /* @__PURE__ */ jsxRuntime.jsx(
18293
- LayoutRenderer,
18294
- {
18295
- item: subItem,
18296
- widgets,
18297
- values,
18298
- droppedInputs,
18299
- validationDetails,
18300
- onChange,
18301
- onInputFocus,
18302
- resetKey
18303
- },
18304
- subItem.id
18305
- ))
18306
- },
18307
- cell.id
18308
- )) }, row.id)) }) }) });
18540
+ case JSON_TYPES.TABLE: {
18541
+ const totalRows = item.rows.length;
18542
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "exercise-table-container", children: /* @__PURE__ */ jsxRuntime.jsx("table", { className: "exercise-table", children: /* @__PURE__ */ jsxRuntime.jsx("tbody", { children: item.rows.map((row, rowIndex) => /* @__PURE__ */ jsxRuntime.jsx("tr", { className: "exercise-table-row", style: { backgroundColor: row.backgroundColor }, children: row.cells.map((cell) => {
18543
+ const rowspan = cell.rowspan || 1;
18544
+ const isLastRow = rowIndex + rowspan - 1 >= totalRows - 1;
18545
+ return /* @__PURE__ */ jsxRuntime.jsx(
18546
+ "td",
18547
+ {
18548
+ className: `exercise-table-cell${isLastRow ? " exercise-table-cell--last-row" : ""}`,
18549
+ colSpan: cell.colspan || 1,
18550
+ rowSpan: rowspan,
18551
+ style: { backgroundColor: cell.backgroundColor },
18552
+ children: cell.items.map((subItem) => /* @__PURE__ */ jsxRuntime.jsx(
18553
+ LayoutRenderer,
18554
+ {
18555
+ item: subItem,
18556
+ widgets,
18557
+ values,
18558
+ droppedInputs,
18559
+ validationDetails,
18560
+ onChange,
18561
+ onInputFocus,
18562
+ resetKey
18563
+ },
18564
+ subItem.id
18565
+ ))
18566
+ },
18567
+ cell.id
18568
+ );
18569
+ }) }, row.id)) }) }) });
18570
+ }
18309
18571
  case JSON_TYPES.NUMBER_LINE:
18310
18572
  return /* @__PURE__ */ jsxRuntime.jsx(
18311
18573
  NumberLineRenderer,