@dcg-overseas/number-line 0.1.2 → 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,9 +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 textY = axisY - ry - 6;
469
497
  const arrowHeadH = 7;
470
498
  const arrowHeadHW = 4;
499
+ const MIN_ARROW_SPAN = 18;
500
+ const textY = Math.min(axisY - ry - 6, axisY - MIN_ARROW_SPAN);
471
501
  const arcPath = buildArcPath({ x1, x2, y: axisY, rx, ry });
472
502
  const isSelected = selectedArcIndices.has(g);
473
503
  const hitCursor = tool === "eraser" ? "cell" : tool === "none" ? "pointer" : "default";
@@ -551,8 +581,8 @@ const GroupArcsLayer = React.memo(function GroupArcsLayer2({
551
581
  }
552
582
  return /* @__PURE__ */ jsxRuntime.jsx("g", { className: "nl-arcs-layer", children: arcs });
553
583
  });
554
- function DrawingLayer({ strokes, liveStroke, tool, onStrokeClick }) {
555
- 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: [
556
586
  strokes.map((s) => /* @__PURE__ */ jsxRuntime.jsxs("g", { children: [
557
587
  s.selected && /* @__PURE__ */ jsxRuntime.jsx(
558
588
  "path",
@@ -563,6 +593,7 @@ function DrawingLayer({ strokes, liveStroke, tool, onStrokeClick }) {
563
593
  strokeWidth: 18,
564
594
  strokeLinecap: "round",
565
595
  strokeLinejoin: "round",
596
+ vectorEffect: "non-scaling-stroke",
566
597
  pointerEvents: "none"
567
598
  }
568
599
  ),
@@ -575,6 +606,7 @@ function DrawingLayer({ strokes, liveStroke, tool, onStrokeClick }) {
575
606
  strokeWidth: tool === "eraser" ? 32 : 28,
576
607
  strokeLinecap: "round",
577
608
  strokeLinejoin: "round",
609
+ vectorEffect: "non-scaling-stroke",
578
610
  pointerEvents: "stroke",
579
611
  style: { cursor: tool === "eraser" ? "cell" : tool === "none" ? "pointer" : "default" },
580
612
  onPointerDown: (e) => {
@@ -594,6 +626,7 @@ function DrawingLayer({ strokes, liveStroke, tool, onStrokeClick }) {
594
626
  strokeWidth: 2.5,
595
627
  strokeLinecap: "round",
596
628
  strokeLinejoin: "round",
629
+ vectorEffect: "non-scaling-stroke",
597
630
  pointerEvents: "none"
598
631
  }
599
632
  )
@@ -607,6 +640,7 @@ function DrawingLayer({ strokes, liveStroke, tool, onStrokeClick }) {
607
640
  strokeWidth: 2.5,
608
641
  strokeLinecap: "round",
609
642
  strokeLinejoin: "round",
643
+ vectorEffect: "non-scaling-stroke",
610
644
  pointerEvents: "none",
611
645
  opacity: 0.7
612
646
  }
@@ -615,12 +649,14 @@ function DrawingLayer({ strokes, liveStroke, tool, onStrokeClick }) {
615
649
  }
616
650
  const PAD_LEFT = 30;
617
651
  const PAD_RIGHT = 40;
618
- const AXIS_Y = 130;
619
- const ARC_MAX_HEIGHT = 80;
652
+ const AXIS_BOTTOM_OFFSET = 50;
653
+ const ARC_TOP_PADDING = 16;
654
+ const MIN_ARC_HEIGHT = 24;
620
655
  function NumberLine({ className }) {
621
656
  const {
622
657
  svgRef,
623
658
  containerWidth,
659
+ containerHeight,
624
660
  svgCursor,
625
661
  min,
626
662
  max,
@@ -628,6 +664,9 @@ function NumberLine({ className }) {
628
664
  groupCount,
629
665
  tickStep,
630
666
  arcColors,
667
+ showGroupTicks,
668
+ showMajorTicks,
669
+ showMinorTicks,
631
670
  tool,
632
671
  strokes,
633
672
  liveStroke,
@@ -639,14 +678,18 @@ function NumberLine({ className }) {
639
678
  onStrokeClick,
640
679
  onArcClick
641
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);
642
685
  return /* @__PURE__ */ jsxRuntime.jsxs(
643
686
  "svg",
644
687
  {
645
688
  ref: svgRef,
646
689
  tabIndex: 0,
647
690
  className: className ? `nl-svg ${className}` : "nl-svg",
648
- viewBox: `0 0 ${DESIGN_WIDTH} ${DESIGN_HEIGHT}`,
649
- preserveAspectRatio: "xMidYMid meet",
691
+ viewBox: `0 0 ${w} ${h}`,
692
+ preserveAspectRatio: "none",
650
693
  style: { width: "100%", height: "100%", display: "block", outline: "none", cursor: svgCursor, touchAction: "none" },
651
694
  onPointerDown,
652
695
  onPointerMove,
@@ -657,13 +700,16 @@ function NumberLine({ className }) {
657
700
  {
658
701
  min,
659
702
  max,
660
- axisY: AXIS_Y,
703
+ axisY,
661
704
  padLeft: PAD_LEFT,
662
705
  padRight: PAD_RIGHT,
663
- viewWidth: DESIGN_WIDTH,
664
- containerWidth,
706
+ viewWidth: w,
707
+ containerWidth: w,
665
708
  groupSize,
666
- tickStep
709
+ tickStep,
710
+ showGroupTicks,
711
+ showMajorTicks,
712
+ showMinorTicks
667
713
  }
668
714
  ),
669
715
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -673,11 +719,11 @@ function NumberLine({ className }) {
673
719
  max,
674
720
  groupSize,
675
721
  groupCount,
676
- axisY: AXIS_Y,
722
+ axisY,
677
723
  padLeft: PAD_LEFT,
678
724
  padRight: PAD_RIGHT,
679
- viewWidth: DESIGN_WIDTH,
680
- arcMaxHeight: ARC_MAX_HEIGHT,
725
+ viewWidth: w,
726
+ arcMaxHeight,
681
727
  tool,
682
728
  selectedArcIndices,
683
729
  deletedArcIndices,
@@ -691,6 +737,8 @@ function NumberLine({ className }) {
691
737
  strokes,
692
738
  liveStroke,
693
739
  tool,
740
+ width: w,
741
+ height: h,
694
742
  onStrokeClick
695
743
  }
696
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,9 +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 textY = axisY - ry - 6;
467
495
  const arrowHeadH = 7;
468
496
  const arrowHeadHW = 4;
497
+ const MIN_ARROW_SPAN = 18;
498
+ const textY = Math.min(axisY - ry - 6, axisY - MIN_ARROW_SPAN);
469
499
  const arcPath = buildArcPath({ x1, x2, y: axisY, rx, ry });
470
500
  const isSelected = selectedArcIndices.has(g);
471
501
  const hitCursor = tool === "eraser" ? "cell" : tool === "none" ? "pointer" : "default";
@@ -549,8 +579,8 @@ const GroupArcsLayer = memo(function GroupArcsLayer2({
549
579
  }
550
580
  return /* @__PURE__ */ jsx("g", { className: "nl-arcs-layer", children: arcs });
551
581
  });
552
- function DrawingLayer({ strokes, liveStroke, tool, onStrokeClick }) {
553
- 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: [
554
584
  strokes.map((s) => /* @__PURE__ */ jsxs("g", { children: [
555
585
  s.selected && /* @__PURE__ */ jsx(
556
586
  "path",
@@ -561,6 +591,7 @@ function DrawingLayer({ strokes, liveStroke, tool, onStrokeClick }) {
561
591
  strokeWidth: 18,
562
592
  strokeLinecap: "round",
563
593
  strokeLinejoin: "round",
594
+ vectorEffect: "non-scaling-stroke",
564
595
  pointerEvents: "none"
565
596
  }
566
597
  ),
@@ -573,6 +604,7 @@ function DrawingLayer({ strokes, liveStroke, tool, onStrokeClick }) {
573
604
  strokeWidth: tool === "eraser" ? 32 : 28,
574
605
  strokeLinecap: "round",
575
606
  strokeLinejoin: "round",
607
+ vectorEffect: "non-scaling-stroke",
576
608
  pointerEvents: "stroke",
577
609
  style: { cursor: tool === "eraser" ? "cell" : tool === "none" ? "pointer" : "default" },
578
610
  onPointerDown: (e) => {
@@ -592,6 +624,7 @@ function DrawingLayer({ strokes, liveStroke, tool, onStrokeClick }) {
592
624
  strokeWidth: 2.5,
593
625
  strokeLinecap: "round",
594
626
  strokeLinejoin: "round",
627
+ vectorEffect: "non-scaling-stroke",
595
628
  pointerEvents: "none"
596
629
  }
597
630
  )
@@ -605,6 +638,7 @@ function DrawingLayer({ strokes, liveStroke, tool, onStrokeClick }) {
605
638
  strokeWidth: 2.5,
606
639
  strokeLinecap: "round",
607
640
  strokeLinejoin: "round",
641
+ vectorEffect: "non-scaling-stroke",
608
642
  pointerEvents: "none",
609
643
  opacity: 0.7
610
644
  }
@@ -613,12 +647,14 @@ function DrawingLayer({ strokes, liveStroke, tool, onStrokeClick }) {
613
647
  }
614
648
  const PAD_LEFT = 30;
615
649
  const PAD_RIGHT = 40;
616
- const AXIS_Y = 130;
617
- const ARC_MAX_HEIGHT = 80;
650
+ const AXIS_BOTTOM_OFFSET = 50;
651
+ const ARC_TOP_PADDING = 16;
652
+ const MIN_ARC_HEIGHT = 24;
618
653
  function NumberLine({ className }) {
619
654
  const {
620
655
  svgRef,
621
656
  containerWidth,
657
+ containerHeight,
622
658
  svgCursor,
623
659
  min,
624
660
  max,
@@ -626,6 +662,9 @@ function NumberLine({ className }) {
626
662
  groupCount,
627
663
  tickStep,
628
664
  arcColors,
665
+ showGroupTicks,
666
+ showMajorTicks,
667
+ showMinorTicks,
629
668
  tool,
630
669
  strokes,
631
670
  liveStroke,
@@ -637,14 +676,18 @@ function NumberLine({ className }) {
637
676
  onStrokeClick,
638
677
  onArcClick
639
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);
640
683
  return /* @__PURE__ */ jsxs(
641
684
  "svg",
642
685
  {
643
686
  ref: svgRef,
644
687
  tabIndex: 0,
645
688
  className: className ? `nl-svg ${className}` : "nl-svg",
646
- viewBox: `0 0 ${DESIGN_WIDTH} ${DESIGN_HEIGHT}`,
647
- preserveAspectRatio: "xMidYMid meet",
689
+ viewBox: `0 0 ${w} ${h}`,
690
+ preserveAspectRatio: "none",
648
691
  style: { width: "100%", height: "100%", display: "block", outline: "none", cursor: svgCursor, touchAction: "none" },
649
692
  onPointerDown,
650
693
  onPointerMove,
@@ -655,13 +698,16 @@ function NumberLine({ className }) {
655
698
  {
656
699
  min,
657
700
  max,
658
- axisY: AXIS_Y,
701
+ axisY,
659
702
  padLeft: PAD_LEFT,
660
703
  padRight: PAD_RIGHT,
661
- viewWidth: DESIGN_WIDTH,
662
- containerWidth,
704
+ viewWidth: w,
705
+ containerWidth: w,
663
706
  groupSize,
664
- tickStep
707
+ tickStep,
708
+ showGroupTicks,
709
+ showMajorTicks,
710
+ showMinorTicks
665
711
  }
666
712
  ),
667
713
  /* @__PURE__ */ jsx(
@@ -671,11 +717,11 @@ function NumberLine({ className }) {
671
717
  max,
672
718
  groupSize,
673
719
  groupCount,
674
- axisY: AXIS_Y,
720
+ axisY,
675
721
  padLeft: PAD_LEFT,
676
722
  padRight: PAD_RIGHT,
677
- viewWidth: DESIGN_WIDTH,
678
- arcMaxHeight: ARC_MAX_HEIGHT,
723
+ viewWidth: w,
724
+ arcMaxHeight,
679
725
  tool,
680
726
  selectedArcIndices,
681
727
  deletedArcIndices,
@@ -689,6 +735,8 @@ function NumberLine({ className }) {
689
735
  strokes,
690
736
  liveStroke,
691
737
  tool,
738
+ width: w,
739
+ height: h,
692
740
  onStrokeClick
693
741
  }
694
742
  )
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dcg-overseas/number-line",
3
- "version": "0.1.2",
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",