@dcg-overseas/number-line 0.1.1 → 0.1.3

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.cjs CHANGED
@@ -47,16 +47,20 @@ function useDrawing() {
47
47
  });
48
48
  setStrokes((prev) => prev.map((s) => ({ ...s, selected: false })));
49
49
  }, []);
50
- const startStroke = React.useCallback((x, y) => {
50
+ const startStroke = React.useCallback((x, y, w, h) => {
51
51
  drawing.current = true;
52
52
  pointCount.current = 1;
53
- currentD.current = `M ${x} ${y}`;
53
+ const xN = w > 0 ? x / w : 0;
54
+ const yN = h > 0 ? y / h : 0;
55
+ currentD.current = `M ${xN} ${yN}`;
54
56
  currentId.current = `${Date.now()}-${Math.random()}`;
55
57
  }, []);
56
- const continueStroke = React.useCallback((x, y) => {
58
+ const continueStroke = React.useCallback((x, y, w, h) => {
57
59
  if (!drawing.current) return null;
58
60
  pointCount.current++;
59
- currentD.current += ` L ${x} ${y}`;
61
+ const xN = w > 0 ? x / w : 0;
62
+ const yN = h > 0 ? y / h : 0;
63
+ currentD.current += ` L ${xN} ${yN}`;
60
64
  return {
61
65
  id: currentId.current,
62
66
  d: currentD.current,
@@ -117,21 +121,29 @@ function useDrawing() {
117
121
  }
118
122
  function useContainerSize() {
119
123
  const ref = React.useRef(null);
120
- const [width, setWidth] = React.useState(600);
124
+ const [size, setSize] = React.useState({ width: 600, height: 200 });
125
+ React.useLayoutEffect(() => {
126
+ const el = ref.current;
127
+ if (!el) return;
128
+ const rect = el.getBoundingClientRect();
129
+ if (rect.width > 0 && rect.height > 0) {
130
+ setSize({ width: rect.width, height: rect.height });
131
+ }
132
+ }, []);
121
133
  React.useEffect(() => {
122
134
  const el = ref.current;
123
135
  if (!el) return;
124
136
  const ro = new ResizeObserver((entries) => {
125
137
  var _a;
126
- const w = (_a = entries[0]) == null ? void 0 : _a.contentRect.width;
127
- if (w && w > 0) setWidth(w);
138
+ const rect = (_a = entries[0]) == null ? void 0 : _a.contentRect;
139
+ if (rect && rect.width > 0 && rect.height > 0) {
140
+ setSize({ width: rect.width, height: rect.height });
141
+ }
128
142
  });
129
143
  ro.observe(el);
130
- const rect = el.getBoundingClientRect();
131
- if (rect.width > 0) setWidth(rect.width);
132
144
  return () => ro.disconnect();
133
145
  }, []);
134
- return { ref, width };
146
+ return { ref, width: size.width, height: size.height };
135
147
  }
136
148
  const NumberLineContext = React.createContext(null);
137
149
  function useNumberLineContext() {
@@ -139,8 +151,6 @@ function useNumberLineContext() {
139
151
  if (!ctx) throw new Error("Must be used inside <NumberLineProvider>");
140
152
  return ctx;
141
153
  }
142
- const DESIGN_WIDTH = 1e3;
143
- const DESIGN_HEIGHT = 200;
144
154
  function NumberLineProvider({
145
155
  min,
146
156
  max,
@@ -148,11 +158,14 @@ function NumberLineProvider({
148
158
  groupCount,
149
159
  tickStep,
150
160
  arcColors,
161
+ showGroupTicks = true,
162
+ showMajorTicks = true,
163
+ showMinorTicks = false,
151
164
  children
152
165
  }) {
153
166
  const [selectedArcIndices, setSelectedArcIndices] = React.useState(/* @__PURE__ */ new Set());
154
167
  const [deletedArcIndices, setDeletedArcIndices] = React.useState(/* @__PURE__ */ new Set());
155
- const { ref: svgRef, width: containerWidth } = useContainerSize();
168
+ const { ref: svgRef, width: containerWidth, height: containerHeight } = useContainerSize();
156
169
  const history = useHistory();
157
170
  const drawing = useDrawing();
158
171
  const [liveStroke, setLiveStroke] = React.useState(null);
@@ -182,19 +195,19 @@ function NumberLineProvider({
182
195
  e.currentTarget.setPointerCapture(e.pointerId);
183
196
  const coords = getLocalCoords(e);
184
197
  if (!coords) return;
185
- drawing.startStroke(...coords);
198
+ drawing.startStroke(coords[0], coords[1], containerWidth, containerHeight);
186
199
  },
187
- [drawing, getLocalCoords]
200
+ [drawing, getLocalCoords, containerWidth, containerHeight]
188
201
  );
189
202
  const onPointerMove = React.useCallback(
190
203
  (e) => {
191
204
  if (drawing.tool !== "pen") return;
192
205
  const coords = getLocalCoords(e);
193
206
  if (!coords) return;
194
- const live = drawing.continueStroke(...coords);
207
+ const live = drawing.continueStroke(coords[0], coords[1], containerWidth, containerHeight);
195
208
  if (live) setLiveStroke(live);
196
209
  },
197
- [drawing, getLocalCoords]
210
+ [drawing, getLocalCoords, containerWidth, containerHeight]
198
211
  );
199
212
  const onPointerUp = React.useCallback(() => {
200
213
  if (drawing.tool !== "pen") return;
@@ -314,8 +327,8 @@ function NumberLineProvider({
314
327
  {
315
328
  value: {
316
329
  svgRef,
317
- viewWidth: DESIGN_WIDTH,
318
330
  containerWidth,
331
+ containerHeight,
319
332
  svgCursor,
320
333
  min,
321
334
  max,
@@ -323,6 +336,9 @@ function NumberLineProvider({
323
336
  groupCount,
324
337
  tickStep,
325
338
  arcColors,
339
+ showGroupTicks,
340
+ showMajorTicks,
341
+ showMinorTicks,
326
342
  tool: drawing.tool,
327
343
  strokes: drawing.strokes,
328
344
  liveStroke,
@@ -362,6 +378,12 @@ const ARC_COLORS = [
362
378
  "#be185d",
363
379
  "#16a34a"
364
380
  ];
381
+ function gcd(a, b) {
382
+ let x = Math.max(1, Math.abs(Math.round(a)));
383
+ let y = Math.max(1, Math.abs(Math.round(b)));
384
+ while (y !== 0) [x, y] = [y, x % y];
385
+ return x;
386
+ }
365
387
  const AxisLayer = React.memo(function AxisLayer2({
366
388
  min,
367
389
  max,
@@ -371,7 +393,10 @@ const AxisLayer = React.memo(function AxisLayer2({
371
393
  viewWidth,
372
394
  containerWidth,
373
395
  groupSize,
374
- tickStep
396
+ tickStep,
397
+ showGroupTicks,
398
+ showMajorTicks,
399
+ showMinorTicks
375
400
  }) {
376
401
  const range = max - min;
377
402
  if (range <= 0) return null;
@@ -381,9 +406,11 @@ const AxisLayer = React.memo(function AxisLayer2({
381
406
  const rawLabelStep = computeLabelStep(containerWidth, range);
382
407
  const labelStep = Math.max(step, Math.ceil(rawLabelStep / step) * step);
383
408
  const MAX_TICKS = 500;
384
- const minorStep = Math.max(1, Math.ceil(range / MAX_TICKS));
409
+ const capStep = Math.max(1, Math.ceil(range / MAX_TICKS));
410
+ const visibleStep = groupSize > 0 ? gcd(step, groupSize) : step;
411
+ const iterStep = showMinorTicks ? capStep : Math.max(capStep, visibleStep);
385
412
  const allTicks = [];
386
- for (let v = min; v <= max; v += minorStep) allTicks.push(v);
413
+ for (let v = min; v <= max; v += iterStep) allTicks.push(v);
387
414
  return /* @__PURE__ */ jsxRuntime.jsxs("g", { className: "nl-axis-layer", children: [
388
415
  /* @__PURE__ */ jsxRuntime.jsx(
389
416
  "line",
@@ -402,6 +429,8 @@ const AxisLayer = React.memo(function AxisLayer2({
402
429
  const isMajor = offset % step === 0;
403
430
  const isGroupBoundary = groupSize > 0 && offset % groupSize === 0;
404
431
  const showLabel = isMajor && offset % labelStep === 0;
432
+ const visible = isGroupBoundary ? showGroupTicks : isMajor ? showMajorTicks : showMinorTicks;
433
+ if (!visible) return null;
405
434
  const tickH = isGroupBoundary ? 10 : isMajor ? 7 : 4;
406
435
  const strokeW = isGroupBoundary ? 1.5 : isMajor ? 1 : 0.5;
407
436
  const color = isMajor ? "#374151" : "#9ca3af";
@@ -465,8 +494,10 @@ const GroupArcsLayer = React.memo(function GroupArcsLayer2({
465
494
  const rx = (x2 - x1) / 2;
466
495
  const ry = Math.min(rx * 0.7, arcMaxHeight);
467
496
  const color = arcColors[g % arcColors.length];
468
- const midX = (x1 + x2) / 2;
469
- const labelY = axisY - ry - 6;
497
+ const arrowHeadH = 7;
498
+ const arrowHeadHW = 4;
499
+ const MIN_ARROW_SPAN = 18;
500
+ const textY = Math.min(axisY - ry - 6, axisY - MIN_ARROW_SPAN);
470
501
  const arcPath = buildArcPath({ x1, x2, y: axisY, rx, ry });
471
502
  const isSelected = selectedArcIndices.has(g);
472
503
  const hitCursor = tool === "eraser" ? "cell" : tool === "none" ? "pointer" : "default";
@@ -514,8 +545,8 @@ const GroupArcsLayer = React.memo(function GroupArcsLayer2({
514
545
  /* @__PURE__ */ jsxRuntime.jsx(
515
546
  "text",
516
547
  {
517
- x: midX,
518
- y: labelY,
548
+ x: x2,
549
+ y: textY,
519
550
  textAnchor: "middle",
520
551
  fontSize: 10,
521
552
  fill: isSelected ? "#1d4ed8" : color,
@@ -524,14 +555,34 @@ const GroupArcsLayer = React.memo(function GroupArcsLayer2({
524
555
  style: { userSelect: "none", WebkitUserSelect: "none" },
525
556
  children: end
526
557
  }
558
+ ),
559
+ /* @__PURE__ */ jsxRuntime.jsx(
560
+ "line",
561
+ {
562
+ x1: x2,
563
+ y1: textY + 4,
564
+ x2,
565
+ y2: axisY - arrowHeadH,
566
+ stroke: isSelected ? "#1d4ed8" : color,
567
+ strokeWidth: 1.5,
568
+ pointerEvents: "none"
569
+ }
570
+ ),
571
+ /* @__PURE__ */ jsxRuntime.jsx(
572
+ "polygon",
573
+ {
574
+ points: `${x2},${axisY} ${x2 - arrowHeadHW},${axisY - arrowHeadH} ${x2 + arrowHeadHW},${axisY - arrowHeadH}`,
575
+ fill: isSelected ? "#1d4ed8" : color,
576
+ pointerEvents: "none"
577
+ }
527
578
  )
528
579
  ] }, g)
529
580
  );
530
581
  }
531
582
  return /* @__PURE__ */ jsxRuntime.jsx("g", { className: "nl-arcs-layer", children: arcs });
532
583
  });
533
- function DrawingLayer({ strokes, liveStroke, tool, onStrokeClick }) {
534
- return /* @__PURE__ */ jsxRuntime.jsxs("g", { className: "nl-drawing-layer", children: [
584
+ function DrawingLayer({ strokes, liveStroke, tool, width, height, onStrokeClick }) {
585
+ return /* @__PURE__ */ jsxRuntime.jsxs("g", { className: "nl-drawing-layer", transform: `scale(${width} ${height})`, children: [
535
586
  strokes.map((s) => /* @__PURE__ */ jsxRuntime.jsxs("g", { children: [
536
587
  s.selected && /* @__PURE__ */ jsxRuntime.jsx(
537
588
  "path",
@@ -542,6 +593,7 @@ function DrawingLayer({ strokes, liveStroke, tool, onStrokeClick }) {
542
593
  strokeWidth: 18,
543
594
  strokeLinecap: "round",
544
595
  strokeLinejoin: "round",
596
+ vectorEffect: "non-scaling-stroke",
545
597
  pointerEvents: "none"
546
598
  }
547
599
  ),
@@ -554,6 +606,7 @@ function DrawingLayer({ strokes, liveStroke, tool, onStrokeClick }) {
554
606
  strokeWidth: tool === "eraser" ? 32 : 28,
555
607
  strokeLinecap: "round",
556
608
  strokeLinejoin: "round",
609
+ vectorEffect: "non-scaling-stroke",
557
610
  pointerEvents: "stroke",
558
611
  style: { cursor: tool === "eraser" ? "cell" : tool === "none" ? "pointer" : "default" },
559
612
  onPointerDown: (e) => {
@@ -573,6 +626,7 @@ function DrawingLayer({ strokes, liveStroke, tool, onStrokeClick }) {
573
626
  strokeWidth: 2.5,
574
627
  strokeLinecap: "round",
575
628
  strokeLinejoin: "round",
629
+ vectorEffect: "non-scaling-stroke",
576
630
  pointerEvents: "none"
577
631
  }
578
632
  )
@@ -586,6 +640,7 @@ function DrawingLayer({ strokes, liveStroke, tool, onStrokeClick }) {
586
640
  strokeWidth: 2.5,
587
641
  strokeLinecap: "round",
588
642
  strokeLinejoin: "round",
643
+ vectorEffect: "non-scaling-stroke",
589
644
  pointerEvents: "none",
590
645
  opacity: 0.7
591
646
  }
@@ -594,12 +649,14 @@ function DrawingLayer({ strokes, liveStroke, tool, onStrokeClick }) {
594
649
  }
595
650
  const PAD_LEFT = 30;
596
651
  const PAD_RIGHT = 40;
597
- const AXIS_Y = 130;
598
- const ARC_MAX_HEIGHT = 80;
652
+ const AXIS_BOTTOM_OFFSET = 50;
653
+ const ARC_TOP_PADDING = 16;
654
+ const MIN_ARC_HEIGHT = 24;
599
655
  function NumberLine({ className }) {
600
656
  const {
601
657
  svgRef,
602
658
  containerWidth,
659
+ containerHeight,
603
660
  svgCursor,
604
661
  min,
605
662
  max,
@@ -607,6 +664,9 @@ function NumberLine({ className }) {
607
664
  groupCount,
608
665
  tickStep,
609
666
  arcColors,
667
+ showGroupTicks,
668
+ showMajorTicks,
669
+ showMinorTicks,
610
670
  tool,
611
671
  strokes,
612
672
  liveStroke,
@@ -618,14 +678,18 @@ function NumberLine({ className }) {
618
678
  onStrokeClick,
619
679
  onArcClick
620
680
  } = useNumberLineContext();
681
+ const w = containerWidth;
682
+ const h = containerHeight;
683
+ const axisY = Math.max(MIN_ARC_HEIGHT + ARC_TOP_PADDING, h - AXIS_BOTTOM_OFFSET);
684
+ const arcMaxHeight = Math.max(MIN_ARC_HEIGHT, axisY - ARC_TOP_PADDING);
621
685
  return /* @__PURE__ */ jsxRuntime.jsxs(
622
686
  "svg",
623
687
  {
624
688
  ref: svgRef,
625
689
  tabIndex: 0,
626
690
  className: className ? `nl-svg ${className}` : "nl-svg",
627
- viewBox: `0 0 ${DESIGN_WIDTH} ${DESIGN_HEIGHT}`,
628
- preserveAspectRatio: "xMidYMid meet",
691
+ viewBox: `0 0 ${w} ${h}`,
692
+ preserveAspectRatio: "none",
629
693
  style: { width: "100%", height: "100%", display: "block", outline: "none", cursor: svgCursor, touchAction: "none" },
630
694
  onPointerDown,
631
695
  onPointerMove,
@@ -636,13 +700,16 @@ function NumberLine({ className }) {
636
700
  {
637
701
  min,
638
702
  max,
639
- axisY: AXIS_Y,
703
+ axisY,
640
704
  padLeft: PAD_LEFT,
641
705
  padRight: PAD_RIGHT,
642
- viewWidth: DESIGN_WIDTH,
643
- containerWidth,
706
+ viewWidth: w,
707
+ containerWidth: w,
644
708
  groupSize,
645
- tickStep
709
+ tickStep,
710
+ showGroupTicks,
711
+ showMajorTicks,
712
+ showMinorTicks
646
713
  }
647
714
  ),
648
715
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -652,11 +719,11 @@ function NumberLine({ className }) {
652
719
  max,
653
720
  groupSize,
654
721
  groupCount,
655
- axisY: AXIS_Y,
722
+ axisY,
656
723
  padLeft: PAD_LEFT,
657
724
  padRight: PAD_RIGHT,
658
- viewWidth: DESIGN_WIDTH,
659
- arcMaxHeight: ARC_MAX_HEIGHT,
725
+ viewWidth: w,
726
+ arcMaxHeight,
660
727
  tool,
661
728
  selectedArcIndices,
662
729
  deletedArcIndices,
@@ -670,6 +737,8 @@ function NumberLine({ className }) {
670
737
  strokes,
671
738
  liveStroke,
672
739
  tool,
740
+ width: w,
741
+ height: h,
673
742
  onStrokeClick
674
743
  }
675
744
  )
package/dist/index.d.ts CHANGED
@@ -7,8 +7,8 @@ export declare function NumberLine({ className }: {
7
7
 
8
8
  export declare interface NumberLineContextValue {
9
9
  svgRef: default_2.RefObject<SVGSVGElement>;
10
- viewWidth: number;
11
10
  containerWidth: number;
11
+ containerHeight: number;
12
12
  svgCursor: string;
13
13
  min: number;
14
14
  max: number;
@@ -16,6 +16,9 @@ export declare interface NumberLineContextValue {
16
16
  groupCount: number;
17
17
  tickStep: number;
18
18
  arcColors?: string[];
19
+ showGroupTicks: boolean;
20
+ showMajorTicks: boolean;
21
+ showMinorTicks: boolean;
19
22
  tool: Tool;
20
23
  strokes: Stroke[];
21
24
  liveStroke: Stroke | null;
@@ -42,9 +45,15 @@ export declare interface NumberLineProps {
42
45
  groupCount: number;
43
46
  tickStep: number;
44
47
  arcColors?: string[];
48
+ /** show group boundary ticks (tallest). Default: true */
49
+ showGroupTicks?: boolean;
50
+ /** show major ticks. Default: true */
51
+ showMajorTicks?: boolean;
52
+ /** show minor ticks (in-between). Default: false */
53
+ showMinorTicks?: boolean;
45
54
  }
46
55
 
47
- export declare function NumberLineProvider({ min, max, groupSize, groupCount, tickStep, arcColors, children, }: NumberLineProviderProps): JSX_2.Element;
56
+ export declare function NumberLineProvider({ min, max, groupSize, groupCount, tickStep, arcColors, showGroupTicks, showMajorTicks, showMinorTicks, children, }: NumberLineProviderProps): JSX_2.Element;
48
57
 
49
58
  declare interface NumberLineProviderProps extends NumberLineProps {
50
59
  children: default_2.ReactNode;
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { jsx, jsxs } from "react/jsx-runtime";
2
- import React, { useRef, useState, useCallback, useEffect, createContext, useContext, memo } from "react";
2
+ import React, { useRef, useState, useCallback, useLayoutEffect, useEffect, createContext, useContext, memo } from "react";
3
3
  function useHistory() {
4
4
  const stackRef = useRef([]);
5
5
  const [stackLen, setStackLen] = useState(0);
@@ -45,16 +45,20 @@ function useDrawing() {
45
45
  });
46
46
  setStrokes((prev) => prev.map((s) => ({ ...s, selected: false })));
47
47
  }, []);
48
- const startStroke = useCallback((x, y) => {
48
+ const startStroke = useCallback((x, y, w, h) => {
49
49
  drawing.current = true;
50
50
  pointCount.current = 1;
51
- currentD.current = `M ${x} ${y}`;
51
+ const xN = w > 0 ? x / w : 0;
52
+ const yN = h > 0 ? y / h : 0;
53
+ currentD.current = `M ${xN} ${yN}`;
52
54
  currentId.current = `${Date.now()}-${Math.random()}`;
53
55
  }, []);
54
- const continueStroke = useCallback((x, y) => {
56
+ const continueStroke = useCallback((x, y, w, h) => {
55
57
  if (!drawing.current) return null;
56
58
  pointCount.current++;
57
- currentD.current += ` L ${x} ${y}`;
59
+ const xN = w > 0 ? x / w : 0;
60
+ const yN = h > 0 ? y / h : 0;
61
+ currentD.current += ` L ${xN} ${yN}`;
58
62
  return {
59
63
  id: currentId.current,
60
64
  d: currentD.current,
@@ -115,21 +119,29 @@ function useDrawing() {
115
119
  }
116
120
  function useContainerSize() {
117
121
  const ref = useRef(null);
118
- const [width, setWidth] = useState(600);
122
+ const [size, setSize] = useState({ width: 600, height: 200 });
123
+ useLayoutEffect(() => {
124
+ const el = ref.current;
125
+ if (!el) return;
126
+ const rect = el.getBoundingClientRect();
127
+ if (rect.width > 0 && rect.height > 0) {
128
+ setSize({ width: rect.width, height: rect.height });
129
+ }
130
+ }, []);
119
131
  useEffect(() => {
120
132
  const el = ref.current;
121
133
  if (!el) return;
122
134
  const ro = new ResizeObserver((entries) => {
123
135
  var _a;
124
- const w = (_a = entries[0]) == null ? void 0 : _a.contentRect.width;
125
- if (w && w > 0) setWidth(w);
136
+ const rect = (_a = entries[0]) == null ? void 0 : _a.contentRect;
137
+ if (rect && rect.width > 0 && rect.height > 0) {
138
+ setSize({ width: rect.width, height: rect.height });
139
+ }
126
140
  });
127
141
  ro.observe(el);
128
- const rect = el.getBoundingClientRect();
129
- if (rect.width > 0) setWidth(rect.width);
130
142
  return () => ro.disconnect();
131
143
  }, []);
132
- return { ref, width };
144
+ return { ref, width: size.width, height: size.height };
133
145
  }
134
146
  const NumberLineContext = createContext(null);
135
147
  function useNumberLineContext() {
@@ -137,8 +149,6 @@ function useNumberLineContext() {
137
149
  if (!ctx) throw new Error("Must be used inside <NumberLineProvider>");
138
150
  return ctx;
139
151
  }
140
- const DESIGN_WIDTH = 1e3;
141
- const DESIGN_HEIGHT = 200;
142
152
  function NumberLineProvider({
143
153
  min,
144
154
  max,
@@ -146,11 +156,14 @@ function NumberLineProvider({
146
156
  groupCount,
147
157
  tickStep,
148
158
  arcColors,
159
+ showGroupTicks = true,
160
+ showMajorTicks = true,
161
+ showMinorTicks = false,
149
162
  children
150
163
  }) {
151
164
  const [selectedArcIndices, setSelectedArcIndices] = useState(/* @__PURE__ */ new Set());
152
165
  const [deletedArcIndices, setDeletedArcIndices] = useState(/* @__PURE__ */ new Set());
153
- const { ref: svgRef, width: containerWidth } = useContainerSize();
166
+ const { ref: svgRef, width: containerWidth, height: containerHeight } = useContainerSize();
154
167
  const history = useHistory();
155
168
  const drawing = useDrawing();
156
169
  const [liveStroke, setLiveStroke] = useState(null);
@@ -180,19 +193,19 @@ function NumberLineProvider({
180
193
  e.currentTarget.setPointerCapture(e.pointerId);
181
194
  const coords = getLocalCoords(e);
182
195
  if (!coords) return;
183
- drawing.startStroke(...coords);
196
+ drawing.startStroke(coords[0], coords[1], containerWidth, containerHeight);
184
197
  },
185
- [drawing, getLocalCoords]
198
+ [drawing, getLocalCoords, containerWidth, containerHeight]
186
199
  );
187
200
  const onPointerMove = useCallback(
188
201
  (e) => {
189
202
  if (drawing.tool !== "pen") return;
190
203
  const coords = getLocalCoords(e);
191
204
  if (!coords) return;
192
- const live = drawing.continueStroke(...coords);
205
+ const live = drawing.continueStroke(coords[0], coords[1], containerWidth, containerHeight);
193
206
  if (live) setLiveStroke(live);
194
207
  },
195
- [drawing, getLocalCoords]
208
+ [drawing, getLocalCoords, containerWidth, containerHeight]
196
209
  );
197
210
  const onPointerUp = useCallback(() => {
198
211
  if (drawing.tool !== "pen") return;
@@ -312,8 +325,8 @@ function NumberLineProvider({
312
325
  {
313
326
  value: {
314
327
  svgRef,
315
- viewWidth: DESIGN_WIDTH,
316
328
  containerWidth,
329
+ containerHeight,
317
330
  svgCursor,
318
331
  min,
319
332
  max,
@@ -321,6 +334,9 @@ function NumberLineProvider({
321
334
  groupCount,
322
335
  tickStep,
323
336
  arcColors,
337
+ showGroupTicks,
338
+ showMajorTicks,
339
+ showMinorTicks,
324
340
  tool: drawing.tool,
325
341
  strokes: drawing.strokes,
326
342
  liveStroke,
@@ -360,6 +376,12 @@ const ARC_COLORS = [
360
376
  "#be185d",
361
377
  "#16a34a"
362
378
  ];
379
+ function gcd(a, b) {
380
+ let x = Math.max(1, Math.abs(Math.round(a)));
381
+ let y = Math.max(1, Math.abs(Math.round(b)));
382
+ while (y !== 0) [x, y] = [y, x % y];
383
+ return x;
384
+ }
363
385
  const AxisLayer = React.memo(function AxisLayer2({
364
386
  min,
365
387
  max,
@@ -369,7 +391,10 @@ const AxisLayer = React.memo(function AxisLayer2({
369
391
  viewWidth,
370
392
  containerWidth,
371
393
  groupSize,
372
- tickStep
394
+ tickStep,
395
+ showGroupTicks,
396
+ showMajorTicks,
397
+ showMinorTicks
373
398
  }) {
374
399
  const range = max - min;
375
400
  if (range <= 0) return null;
@@ -379,9 +404,11 @@ const AxisLayer = React.memo(function AxisLayer2({
379
404
  const rawLabelStep = computeLabelStep(containerWidth, range);
380
405
  const labelStep = Math.max(step, Math.ceil(rawLabelStep / step) * step);
381
406
  const MAX_TICKS = 500;
382
- const minorStep = Math.max(1, Math.ceil(range / MAX_TICKS));
407
+ const capStep = Math.max(1, Math.ceil(range / MAX_TICKS));
408
+ const visibleStep = groupSize > 0 ? gcd(step, groupSize) : step;
409
+ const iterStep = showMinorTicks ? capStep : Math.max(capStep, visibleStep);
383
410
  const allTicks = [];
384
- for (let v = min; v <= max; v += minorStep) allTicks.push(v);
411
+ for (let v = min; v <= max; v += iterStep) allTicks.push(v);
385
412
  return /* @__PURE__ */ jsxs("g", { className: "nl-axis-layer", children: [
386
413
  /* @__PURE__ */ jsx(
387
414
  "line",
@@ -400,6 +427,8 @@ const AxisLayer = React.memo(function AxisLayer2({
400
427
  const isMajor = offset % step === 0;
401
428
  const isGroupBoundary = groupSize > 0 && offset % groupSize === 0;
402
429
  const showLabel = isMajor && offset % labelStep === 0;
430
+ const visible = isGroupBoundary ? showGroupTicks : isMajor ? showMajorTicks : showMinorTicks;
431
+ if (!visible) return null;
403
432
  const tickH = isGroupBoundary ? 10 : isMajor ? 7 : 4;
404
433
  const strokeW = isGroupBoundary ? 1.5 : isMajor ? 1 : 0.5;
405
434
  const color = isMajor ? "#374151" : "#9ca3af";
@@ -463,8 +492,10 @@ const GroupArcsLayer = memo(function GroupArcsLayer2({
463
492
  const rx = (x2 - x1) / 2;
464
493
  const ry = Math.min(rx * 0.7, arcMaxHeight);
465
494
  const color = arcColors[g % arcColors.length];
466
- const midX = (x1 + x2) / 2;
467
- const labelY = axisY - ry - 6;
495
+ const arrowHeadH = 7;
496
+ const arrowHeadHW = 4;
497
+ const MIN_ARROW_SPAN = 18;
498
+ const textY = Math.min(axisY - ry - 6, axisY - MIN_ARROW_SPAN);
468
499
  const arcPath = buildArcPath({ x1, x2, y: axisY, rx, ry });
469
500
  const isSelected = selectedArcIndices.has(g);
470
501
  const hitCursor = tool === "eraser" ? "cell" : tool === "none" ? "pointer" : "default";
@@ -512,8 +543,8 @@ const GroupArcsLayer = memo(function GroupArcsLayer2({
512
543
  /* @__PURE__ */ jsx(
513
544
  "text",
514
545
  {
515
- x: midX,
516
- y: labelY,
546
+ x: x2,
547
+ y: textY,
517
548
  textAnchor: "middle",
518
549
  fontSize: 10,
519
550
  fill: isSelected ? "#1d4ed8" : color,
@@ -522,14 +553,34 @@ const GroupArcsLayer = memo(function GroupArcsLayer2({
522
553
  style: { userSelect: "none", WebkitUserSelect: "none" },
523
554
  children: end
524
555
  }
556
+ ),
557
+ /* @__PURE__ */ jsx(
558
+ "line",
559
+ {
560
+ x1: x2,
561
+ y1: textY + 4,
562
+ x2,
563
+ y2: axisY - arrowHeadH,
564
+ stroke: isSelected ? "#1d4ed8" : color,
565
+ strokeWidth: 1.5,
566
+ pointerEvents: "none"
567
+ }
568
+ ),
569
+ /* @__PURE__ */ jsx(
570
+ "polygon",
571
+ {
572
+ points: `${x2},${axisY} ${x2 - arrowHeadHW},${axisY - arrowHeadH} ${x2 + arrowHeadHW},${axisY - arrowHeadH}`,
573
+ fill: isSelected ? "#1d4ed8" : color,
574
+ pointerEvents: "none"
575
+ }
525
576
  )
526
577
  ] }, g)
527
578
  );
528
579
  }
529
580
  return /* @__PURE__ */ jsx("g", { className: "nl-arcs-layer", children: arcs });
530
581
  });
531
- function DrawingLayer({ strokes, liveStroke, tool, onStrokeClick }) {
532
- return /* @__PURE__ */ jsxs("g", { className: "nl-drawing-layer", children: [
582
+ function DrawingLayer({ strokes, liveStroke, tool, width, height, onStrokeClick }) {
583
+ return /* @__PURE__ */ jsxs("g", { className: "nl-drawing-layer", transform: `scale(${width} ${height})`, children: [
533
584
  strokes.map((s) => /* @__PURE__ */ jsxs("g", { children: [
534
585
  s.selected && /* @__PURE__ */ jsx(
535
586
  "path",
@@ -540,6 +591,7 @@ function DrawingLayer({ strokes, liveStroke, tool, onStrokeClick }) {
540
591
  strokeWidth: 18,
541
592
  strokeLinecap: "round",
542
593
  strokeLinejoin: "round",
594
+ vectorEffect: "non-scaling-stroke",
543
595
  pointerEvents: "none"
544
596
  }
545
597
  ),
@@ -552,6 +604,7 @@ function DrawingLayer({ strokes, liveStroke, tool, onStrokeClick }) {
552
604
  strokeWidth: tool === "eraser" ? 32 : 28,
553
605
  strokeLinecap: "round",
554
606
  strokeLinejoin: "round",
607
+ vectorEffect: "non-scaling-stroke",
555
608
  pointerEvents: "stroke",
556
609
  style: { cursor: tool === "eraser" ? "cell" : tool === "none" ? "pointer" : "default" },
557
610
  onPointerDown: (e) => {
@@ -571,6 +624,7 @@ function DrawingLayer({ strokes, liveStroke, tool, onStrokeClick }) {
571
624
  strokeWidth: 2.5,
572
625
  strokeLinecap: "round",
573
626
  strokeLinejoin: "round",
627
+ vectorEffect: "non-scaling-stroke",
574
628
  pointerEvents: "none"
575
629
  }
576
630
  )
@@ -584,6 +638,7 @@ function DrawingLayer({ strokes, liveStroke, tool, onStrokeClick }) {
584
638
  strokeWidth: 2.5,
585
639
  strokeLinecap: "round",
586
640
  strokeLinejoin: "round",
641
+ vectorEffect: "non-scaling-stroke",
587
642
  pointerEvents: "none",
588
643
  opacity: 0.7
589
644
  }
@@ -592,12 +647,14 @@ function DrawingLayer({ strokes, liveStroke, tool, onStrokeClick }) {
592
647
  }
593
648
  const PAD_LEFT = 30;
594
649
  const PAD_RIGHT = 40;
595
- const AXIS_Y = 130;
596
- const ARC_MAX_HEIGHT = 80;
650
+ const AXIS_BOTTOM_OFFSET = 50;
651
+ const ARC_TOP_PADDING = 16;
652
+ const MIN_ARC_HEIGHT = 24;
597
653
  function NumberLine({ className }) {
598
654
  const {
599
655
  svgRef,
600
656
  containerWidth,
657
+ containerHeight,
601
658
  svgCursor,
602
659
  min,
603
660
  max,
@@ -605,6 +662,9 @@ function NumberLine({ className }) {
605
662
  groupCount,
606
663
  tickStep,
607
664
  arcColors,
665
+ showGroupTicks,
666
+ showMajorTicks,
667
+ showMinorTicks,
608
668
  tool,
609
669
  strokes,
610
670
  liveStroke,
@@ -616,14 +676,18 @@ function NumberLine({ className }) {
616
676
  onStrokeClick,
617
677
  onArcClick
618
678
  } = useNumberLineContext();
679
+ const w = containerWidth;
680
+ const h = containerHeight;
681
+ const axisY = Math.max(MIN_ARC_HEIGHT + ARC_TOP_PADDING, h - AXIS_BOTTOM_OFFSET);
682
+ const arcMaxHeight = Math.max(MIN_ARC_HEIGHT, axisY - ARC_TOP_PADDING);
619
683
  return /* @__PURE__ */ jsxs(
620
684
  "svg",
621
685
  {
622
686
  ref: svgRef,
623
687
  tabIndex: 0,
624
688
  className: className ? `nl-svg ${className}` : "nl-svg",
625
- viewBox: `0 0 ${DESIGN_WIDTH} ${DESIGN_HEIGHT}`,
626
- preserveAspectRatio: "xMidYMid meet",
689
+ viewBox: `0 0 ${w} ${h}`,
690
+ preserveAspectRatio: "none",
627
691
  style: { width: "100%", height: "100%", display: "block", outline: "none", cursor: svgCursor, touchAction: "none" },
628
692
  onPointerDown,
629
693
  onPointerMove,
@@ -634,13 +698,16 @@ function NumberLine({ className }) {
634
698
  {
635
699
  min,
636
700
  max,
637
- axisY: AXIS_Y,
701
+ axisY,
638
702
  padLeft: PAD_LEFT,
639
703
  padRight: PAD_RIGHT,
640
- viewWidth: DESIGN_WIDTH,
641
- containerWidth,
704
+ viewWidth: w,
705
+ containerWidth: w,
642
706
  groupSize,
643
- tickStep
707
+ tickStep,
708
+ showGroupTicks,
709
+ showMajorTicks,
710
+ showMinorTicks
644
711
  }
645
712
  ),
646
713
  /* @__PURE__ */ jsx(
@@ -650,11 +717,11 @@ function NumberLine({ className }) {
650
717
  max,
651
718
  groupSize,
652
719
  groupCount,
653
- axisY: AXIS_Y,
720
+ axisY,
654
721
  padLeft: PAD_LEFT,
655
722
  padRight: PAD_RIGHT,
656
- viewWidth: DESIGN_WIDTH,
657
- arcMaxHeight: ARC_MAX_HEIGHT,
723
+ viewWidth: w,
724
+ arcMaxHeight,
658
725
  tool,
659
726
  selectedArcIndices,
660
727
  deletedArcIndices,
@@ -668,6 +735,8 @@ function NumberLine({ className }) {
668
735
  strokes,
669
736
  liveStroke,
670
737
  tool,
738
+ width: w,
739
+ height: h,
671
740
  onStrokeClick
672
741
  }
673
742
  )
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dcg-overseas/number-line",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Interactive number line component with group arcs and freehand drawing",
5
5
  "type": "module",
6
6
  "license": "MIT",