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