canvu-react 0.3.39 → 0.3.40

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.
Files changed (45) hide show
  1. package/dist/{asset-hydration-DowNdaOJ.d.cts → asset-hydration-B7yMDQE-.d.cts} +2 -2
  2. package/dist/{asset-hydration-DdFLdlqX.d.ts → asset-hydration-CbwQVAwh.d.ts} +2 -2
  3. package/dist/{camera-Di5R_Rwl.d.cts → camera-CVVG7z56.d.cts} +1 -1
  4. package/dist/{camera-AoTwBSoE.d.ts → camera-CoRYN_IV.d.ts} +1 -1
  5. package/dist/chatbot.d.cts +4 -4
  6. package/dist/chatbot.d.ts +4 -4
  7. package/dist/index.cjs +59 -15
  8. package/dist/index.cjs.map +1 -1
  9. package/dist/index.d.cts +6 -6
  10. package/dist/index.d.ts +6 -6
  11. package/dist/index.js +59 -15
  12. package/dist/index.js.map +1 -1
  13. package/dist/native.cjs +57 -14
  14. package/dist/native.cjs.map +1 -1
  15. package/dist/native.d.cts +2 -2
  16. package/dist/native.d.ts +2 -2
  17. package/dist/native.js +57 -14
  18. package/dist/native.js.map +1 -1
  19. package/dist/react.cjs +699 -255
  20. package/dist/react.cjs.map +1 -1
  21. package/dist/react.d.cts +10 -10
  22. package/dist/react.d.ts +10 -10
  23. package/dist/react.js +699 -255
  24. package/dist/react.js.map +1 -1
  25. package/dist/realtime.cjs +36 -0
  26. package/dist/realtime.cjs.map +1 -1
  27. package/dist/realtime.d.cts +6 -6
  28. package/dist/realtime.d.ts +6 -6
  29. package/dist/realtime.js +36 -0
  30. package/dist/realtime.js.map +1 -1
  31. package/dist/{shape-builders-Dedcl6tw.d.cts → shape-builders-BAWu-PxX.d.cts} +7 -3
  32. package/dist/{shape-builders-C7bxJBGR.d.ts → shape-builders-ClKv9tz9.d.ts} +7 -3
  33. package/dist/tldraw.cjs +56 -14
  34. package/dist/tldraw.cjs.map +1 -1
  35. package/dist/tldraw.d.cts +1 -1
  36. package/dist/tldraw.d.ts +1 -1
  37. package/dist/tldraw.js +56 -14
  38. package/dist/tldraw.js.map +1 -1
  39. package/dist/{types-DUW61Tjy.d.cts → types-BC9Xgfu6.d.cts} +11 -6
  40. package/dist/{types-Bnq2HtHQ.d.cts → types-BCCvY6ie.d.cts} +2 -0
  41. package/dist/{types-Bnq2HtHQ.d.ts → types-BCCvY6ie.d.ts} +2 -0
  42. package/dist/{types-B2Na677H.d.cts → types-BUPc2Zgw.d.cts} +1 -1
  43. package/dist/{types-zmUah-vP.d.ts → types-CYtq9Pr9.d.ts} +1 -1
  44. package/dist/{types-BBb8KoyW.d.ts → types-DlSVGX0w.d.ts} +11 -6
  45. package/package.json +1 -1
package/dist/react.cjs CHANGED
@@ -91,6 +91,12 @@ function lineHeightFor(fontSize) {
91
91
  function firstLineBaselineY(fontSize) {
92
92
  return fontSize * FIRST_LINE_BASELINE_RATIO;
93
93
  }
94
+ function textBaselineYFor(fontSize) {
95
+ return firstLineBaselineY(fontSize);
96
+ }
97
+ function textLineHeightFor(fontSize) {
98
+ return lineHeightFor(fontSize);
99
+ }
94
100
  function measureTextBoundsLocal(content, fontSize = DEFAULT_TEXT_FONT_SIZE) {
95
101
  const cacheKey = textMeasureCacheKey(content, fontSize);
96
102
  const cached = textMeasureCache.get(cacheKey);
@@ -104,7 +110,7 @@ function measureTextBoundsLocal(content, fontSize = DEFAULT_TEXT_FONT_SIZE) {
104
110
  let maxInnerW = 0;
105
111
  const ctx = getSharedMeasureContext();
106
112
  if (ctx) {
107
- ctx.font = `${fontSize}px system-ui, sans-serif`;
113
+ ctx.font = `${fontSize}px ${TEXT_FONT_FAMILY}`;
108
114
  for (const line of lines) {
109
115
  const toMeasure = trimmed.length === 0 ? PLACEHOLDER : line.length === 0 ? " " : line;
110
116
  maxInnerW = Math.max(maxInnerW, ctx.measureText(toMeasure).width);
@@ -120,22 +126,22 @@ function measureTextBoundsLocal(content, fontSize = DEFAULT_TEXT_FONT_SIZE) {
120
126
  const width = Math.max(minW, TEXT_PAD_X * 2 + maxInnerW);
121
127
  const height = Math.max(
122
128
  MIN_TEXT_BOX_H,
123
- baselineY + (lines.length - 1) * lh + Math.max(8, fontSize * 0.35)
129
+ baselineY + (lines.length - 1) * lh + Math.max(4, fontSize * BOTTOM_PAD_RATIO)
124
130
  );
125
131
  const measured = { width, height };
126
132
  cacheMeasuredBounds(cacheKey, measured);
127
133
  return measured;
128
134
  }
129
- function buildTextSvg(content, _width, _height, fillColor = "#2563eb", fontSize = DEFAULT_TEXT_FONT_SIZE) {
135
+ function buildTextSvg(content, _width, _height, fillColor = "#1d1d1d", fontSize = DEFAULT_TEXT_FONT_SIZE) {
130
136
  const lh = lineHeightFor(fontSize);
131
137
  const y0 = firstLineBaselineY(fontSize);
132
138
  const trimmed = content.trim();
133
139
  if (trimmed.length === 0) {
134
- return `<text x="4" y="${y0}" font-size="${fontSize}" font-family="system-ui,sans-serif" fill="#94a3b8" font-style="italic">${escapeSvgTextContent(PLACEHOLDER)}</text>`;
140
+ return `<text x="4" y="${y0}" font-size="${fontSize}" font-family="${TEXT_FONT_FAMILY}" fill="#94a3b8" font-style="italic">${escapeSvgTextContent(PLACEHOLDER)}</text>`;
135
141
  }
136
142
  const lines = content.split("\n");
137
143
  if (lines.length === 1) {
138
- return `<text x="4" y="${y0}" font-size="${fontSize}" font-family="system-ui,sans-serif" fill="${fillColor}">${escapeSvgTextContent(lines[0] ?? "")}</text>`;
144
+ return `<text x="4" y="${y0}" font-size="${fontSize}" font-family="${TEXT_FONT_FAMILY}" fill="${fillColor}">${escapeSvgTextContent(lines[0] ?? "")}</text>`;
139
145
  }
140
146
  const parts = [];
141
147
  for (let i = 0; i < lines.length; i++) {
@@ -146,28 +152,32 @@ function buildTextSvg(content, _width, _height, fillColor = "#2563eb", fontSize
146
152
  parts.push(`<tspan x="4" dy="${lh}">${escapeSvgTextContent(line)}</tspan>`);
147
153
  }
148
154
  }
149
- return `<text x="4" y="${y0}" font-size="${fontSize}" font-family="system-ui,sans-serif" fill="${fillColor}">${parts.join("")}</text>`;
155
+ return `<text x="4" y="${y0}" font-size="${fontSize}" font-family="${TEXT_FONT_FAMILY}" fill="${fillColor}">${parts.join("")}</text>`;
150
156
  }
151
- function buildTextFixedBoundsSvg(content, width, height, fillColor = "#2563eb", fontSize = DEFAULT_TEXT_FONT_SIZE) {
157
+ function buildTextFixedBoundsSvg(content, width, height, fillColor = "#1d1d1d", fontSize = DEFAULT_TEXT_FONT_SIZE) {
152
158
  const w = Math.max(1, width);
153
159
  const h = Math.max(1, height);
154
160
  const lh = lineHeightFor(fontSize);
155
161
  const trimmed = content.trim();
162
+ const padTop = EDIT_TOP_PAD_RATIO * fontSize;
156
163
  if (trimmed.length === 0) {
157
- return `<foreignObject width="${w}" height="${h}"><div xmlns="http://www.w3.org/1999/xhtml" style="box-sizing:border-box;width:100%;height:100%;margin:0;padding:2px 4px;font-size:${fontSize}px;line-height:${lh}px;font-family:system-ui,sans-serif;white-space:pre-wrap;word-wrap:break-word;overflow:hidden;color:#94a3b8;font-style:italic">${escapeHtmlText(PLACEHOLDER)}</div></foreignObject>`;
164
+ return `<foreignObject width="${w}" height="${h}"><div xmlns="http://www.w3.org/1999/xhtml" style="box-sizing:border-box;width:100%;height:100%;margin:0;padding:${padTop}px 4px 0 4px;font-size:${fontSize}px;line-height:${lh}px;font-family:${TEXT_FONT_FAMILY};white-space:pre-wrap;word-wrap:break-word;overflow:hidden;color:#94a3b8;font-style:italic">${escapeHtmlText(PLACEHOLDER)}</div></foreignObject>`;
158
165
  }
159
166
  const body = escapeHtmlText(content);
160
- return `<foreignObject width="${w}" height="${h}"><div xmlns="http://www.w3.org/1999/xhtml" style="box-sizing:border-box;width:100%;height:100%;margin:0;padding:2px 4px;font-size:${fontSize}px;line-height:${lh}px;font-family:system-ui,sans-serif;white-space:pre-wrap;word-wrap:break-word;overflow:hidden;color:${fillColor}">${body}</div></foreignObject>`;
167
+ return `<foreignObject width="${w}" height="${h}"><div xmlns="http://www.w3.org/1999/xhtml" style="box-sizing:border-box;width:100%;height:100%;margin:0;padding:${padTop}px 4px 0 4px;font-size:${fontSize}px;line-height:${lh}px;font-family:${TEXT_FONT_FAMILY};white-space:pre-wrap;word-wrap:break-word;overflow:hidden;color:${fillColor}">${body}</div></foreignObject>`;
161
168
  }
162
- var DEFAULT_TEXT_FONT_SIZE, LINE_HEIGHT_RATIO, FIRST_LINE_BASELINE_RATIO, PLACEHOLDER, MIN_TEXT_BOX_W, MIN_TEXT_BOX_H, TEXT_PAD_X, MAX_TEXT_MEASURE_CACHE_ENTRIES, sharedMeasureContext, textMeasureCache;
169
+ var DEFAULT_TEXT_FONT_SIZE, TEXT_FONT_FAMILY, LINE_HEIGHT_RATIO, FIRST_LINE_BASELINE_RATIO, EDIT_TOP_PAD_RATIO, BOTTOM_PAD_RATIO, PLACEHOLDER, MIN_TEXT_BOX_W, MIN_TEXT_BOX_H, TEXT_PAD_X, MAX_TEXT_MEASURE_CACHE_ENTRIES, sharedMeasureContext, textMeasureCache;
163
170
  var init_text_svg = __esm({
164
171
  "src/scene/text-svg.ts"() {
165
172
  DEFAULT_TEXT_FONT_SIZE = 18;
173
+ TEXT_FONT_FAMILY = "Inter, -apple-system, BlinkMacSystemFont, ui-sans-serif, system-ui, sans-serif";
166
174
  LINE_HEIGHT_RATIO = 22 / 18;
167
- FIRST_LINE_BASELINE_RATIO = 24 / 18;
175
+ FIRST_LINE_BASELINE_RATIO = 20 / 18;
176
+ EDIT_TOP_PAD_RATIO = 4 / 18;
177
+ BOTTOM_PAD_RATIO = 4 / 18;
168
178
  PLACEHOLDER = "Tap to type";
169
179
  MIN_TEXT_BOX_W = 40;
170
- MIN_TEXT_BOX_H = 36;
180
+ MIN_TEXT_BOX_H = 26;
171
181
  TEXT_PAD_X = 4;
172
182
  MAX_TEXT_MEASURE_CACHE_ENTRIES = 2e3;
173
183
  textMeasureCache = /* @__PURE__ */ new Map();
@@ -280,7 +290,8 @@ function resolveStrokeStyle(item) {
280
290
  return {
281
291
  stroke: item.stroke ?? DEFAULT_STROKE_STYLE.stroke,
282
292
  strokeWidth: item.strokeWidth ?? DEFAULT_STROKE_STYLE.strokeWidth,
283
- strokeOpacity: item.strokeOpacity
293
+ strokeOpacity: item.strokeOpacity,
294
+ strokeDash: item.strokeDash
284
295
  };
285
296
  }
286
297
  function strokeOpacityAttr(style) {
@@ -603,6 +614,30 @@ function buildDrawDotSvg(r, style = DEFAULT_STROKE_STYLE) {
603
614
  const op = style.strokeOpacity != null ? ` fill-opacity="${style.strokeOpacity}"` : "";
604
615
  return `<circle cx="${r}" cy="${r}" r="${r}" fill="${style.stroke}" shape-rendering="geometricPrecision"${op} />`;
605
616
  }
617
+ function dashArrayForDrawStroke(strokeWidth) {
618
+ const dash = Math.max(strokeWidth * 1.8, 4);
619
+ const gap = Math.max(strokeWidth * 1.4, 3);
620
+ return `${dash} ${gap}`;
621
+ }
622
+ function buildSmoothedCenterlinePath(points) {
623
+ if (points.length < 2) return null;
624
+ const first = points[0];
625
+ if (!first) return null;
626
+ let d = `M ${first.x} ${first.y}`;
627
+ for (let i = 1; i < points.length - 1; i++) {
628
+ const a = points[i];
629
+ const b = points[i + 1];
630
+ if (!a || !b) continue;
631
+ const midX = (a.x + b.x) / 2;
632
+ const midY = (a.y + b.y) / 2;
633
+ d += ` Q ${a.x} ${a.y} ${midX} ${midY}`;
634
+ }
635
+ const last = points[points.length - 1];
636
+ if (last) {
637
+ d += ` L ${last.x} ${last.y}`;
638
+ }
639
+ return d;
640
+ }
606
641
  function computeFreehandSvgPayload(pathPointsLocal, style, toolKind, strokeComplete = true) {
607
642
  if (pathPointsLocal.length === 0) return null;
608
643
  if (pathPointsLocal.length === 1) {
@@ -617,6 +652,18 @@ function computeFreehandSvgPayload(pathPointsLocal, style, toolKind, strokeCompl
617
652
  fillOpacity: style.strokeOpacity
618
653
  };
619
654
  }
655
+ if (style.strokeDash === "dashed" && (toolKind === "draw" || toolKind === "pencil")) {
656
+ const d2 = buildSmoothedCenterlinePath(pathPointsLocal);
657
+ if (!d2) return null;
658
+ return {
659
+ kind: "strokePath",
660
+ d: d2,
661
+ stroke: style.stroke,
662
+ strokeWidth: style.strokeWidth,
663
+ strokeOpacity: style.strokeOpacity,
664
+ strokeDasharray: dashArrayForDrawStroke(style.strokeWidth)
665
+ };
666
+ }
620
667
  const hasPressure = pathPointsLocal.some(
621
668
  (p) => p.pressure != null && Number.isFinite(p.pressure)
622
669
  );
@@ -659,7 +706,8 @@ function freehandPayloadToSvgString(payload) {
659
706
  strokeWidth: payload.strokeWidth,
660
707
  strokeOpacity: payload.strokeOpacity
661
708
  });
662
- return `<path d="${payload.d}" fill="none" stroke="${payload.stroke}" stroke-width="${payload.strokeWidth}" stroke-linecap="round" stroke-linejoin="round" shape-rendering="geometricPrecision"${op} />`;
709
+ const dash = payload.strokeDasharray ? ` stroke-dasharray="${payload.strokeDasharray}"` : "";
710
+ return `<path d="${payload.d}" fill="none" stroke="${payload.stroke}" stroke-width="${payload.strokeWidth}" stroke-linecap="round" stroke-linejoin="round" shape-rendering="geometricPrecision"${op}${dash} />`;
663
711
  }
664
712
  function buildFreehandPathSvg(pathPointsLocal, style, toolKind, strokeComplete = true) {
665
713
  const payload = computeFreehandSvgPayload(
@@ -871,7 +919,7 @@ function createDrawDotItem(id, worldX, worldY, radius, style) {
871
919
  childrenSvg: ""
872
920
  });
873
921
  }
874
- function createTextItem(id, bounds, text = "", style) {
922
+ function createTextItem(id, bounds, text = "", style, textFontSize) {
875
923
  const r = normalizeRect(bounds);
876
924
  const s = { ...DEFAULT_STROKE_STYLE, ...style };
877
925
  return rebuildItemSvg({
@@ -884,6 +932,7 @@ function createTextItem(id, bounds, text = "", style) {
884
932
  stroke: s.stroke,
885
933
  strokeWidth: s.strokeWidth,
886
934
  ...s.strokeOpacity != null ? { strokeOpacity: s.strokeOpacity } : {},
935
+ ...textFontSize != null ? { textFontSize } : {},
887
936
  childrenSvg: ""
888
937
  });
889
938
  }
@@ -920,6 +969,7 @@ function createFreehandStrokeItem(id, pointsWorld, toolKind, style) {
920
969
  stroke: merged.stroke,
921
970
  strokeWidth: merged.strokeWidth,
922
971
  ...merged.strokeOpacity != null ? { strokeOpacity: merged.strokeOpacity } : {},
972
+ ...merged.strokeDash != null ? { strokeDash: merged.strokeDash } : {},
923
973
  pathPointsLocal,
924
974
  childrenSvg: ""
925
975
  });
@@ -996,7 +1046,7 @@ var init_shape_builders = __esm({
996
1046
  init_custom_shape();
997
1047
  init_text_svg();
998
1048
  DEFAULT_STROKE_STYLE = {
999
- stroke: "#2563eb",
1049
+ stroke: "#1d1d1d",
1000
1050
  strokeWidth: 2
1001
1051
  };
1002
1052
  TOOL_FREEHAND_DEFAULTS = {
@@ -3488,33 +3538,416 @@ var DEFAULT_VECTOR_TOOLS = [
3488
3538
  shortcutHint: "I"
3489
3539
  }
3490
3540
  ];
3541
+
3542
+ // src/react/VectorSelectionInspector.tsx
3543
+ init_text_svg();
3491
3544
  var shellLook = {
3492
3545
  display: "flex",
3493
3546
  flexDirection: "column",
3494
- gap: 8,
3495
- minWidth: 160,
3496
- padding: "10px 12px",
3497
- borderRadius: 8,
3498
- border: "1px solid rgba(0,0,0,0.12)",
3499
- background: "rgba(255,255,255,0.96)",
3500
- boxShadow: "0 1px 3px rgba(0,0,0,0.08)",
3547
+ gap: 10,
3548
+ minWidth: 200,
3549
+ padding: "12px 14px",
3550
+ borderRadius: 10,
3551
+ border: "1px solid rgba(0,0,0,0.1)",
3552
+ background: "rgba(255,255,255,0.97)",
3553
+ boxShadow: "0 4px 16px rgba(0,0,0,0.06), 0 1px 3px rgba(0,0,0,0.08)",
3501
3554
  pointerEvents: "auto"
3502
3555
  };
3503
- var labelStyle2 = {
3556
+ var sectionLabelStyle = {
3504
3557
  display: "flex",
3505
3558
  flexDirection: "column",
3506
- gap: 4,
3559
+ gap: 6,
3507
3560
  fontSize: 11,
3508
3561
  fontWeight: 600,
3509
- color: "#52525b"
3562
+ color: "#52525b",
3563
+ letterSpacing: "0.02em",
3564
+ textTransform: "uppercase"
3510
3565
  };
3566
+ var mixedHintStyle = {
3567
+ fontWeight: 400,
3568
+ color: "#a1a1aa",
3569
+ textTransform: "none",
3570
+ letterSpacing: 0
3571
+ };
3572
+ var TLDRAW_PALETTE = [
3573
+ { name: "black", hex: "#1d1d1d" },
3574
+ { name: "grey", hex: "#9fa8b2" },
3575
+ { name: "light-violet", hex: "#e085f4" },
3576
+ { name: "violet", hex: "#ae3ec9" },
3577
+ { name: "blue", hex: "#4263eb" },
3578
+ { name: "light-blue", hex: "#4dabf7" },
3579
+ { name: "yellow", hex: "#ffc078" },
3580
+ { name: "orange", hex: "#f76707" },
3581
+ { name: "green", hex: "#099268" },
3582
+ { name: "light-green", hex: "#40c057" },
3583
+ { name: "light-red", hex: "#ff8787" },
3584
+ { name: "red", hex: "#e03131" }
3585
+ ];
3511
3586
  function normalizeHex(stroke) {
3512
3587
  if (/^#[0-9A-Fa-f]{6}$/.test(stroke)) return stroke;
3513
- return "#2563eb";
3588
+ return "#1d1d1d";
3589
+ }
3590
+ function hexesEqual(a, b) {
3591
+ return a.toLowerCase() === b.toLowerCase();
3514
3592
  }
3515
3593
  function isStylableKind(tk) {
3516
3594
  return tk === "rect" || tk === "ellipse" || tk === "architectural-cloud" || tk === "line" || tk === "arrow" || tk === "draw" || tk === "pencil" || tk === "brush" || tk === "marker" || tk === "text";
3517
3595
  }
3596
+ var FONT_SIZE_PRESETS = [
3597
+ { label: "S", size: 14 },
3598
+ { label: "M", size: 18 },
3599
+ { label: "L", size: 28 },
3600
+ { label: "XL", size: 44 }
3601
+ ];
3602
+ function ColorSwatch({ color, selected, onSelect }) {
3603
+ return /* @__PURE__ */ jsxRuntime.jsx(
3604
+ "button",
3605
+ {
3606
+ type: "button",
3607
+ "aria-label": color.name,
3608
+ "aria-pressed": selected,
3609
+ onClick: onSelect,
3610
+ onPointerDown: (e) => {
3611
+ if (e.pointerType === "mouse") e.preventDefault();
3612
+ },
3613
+ style: {
3614
+ width: 24,
3615
+ height: 24,
3616
+ padding: 0,
3617
+ borderRadius: "50%",
3618
+ border: selected ? "2px solid #18181b" : "1px solid rgba(0,0,0,0.12)",
3619
+ background: color.hex,
3620
+ cursor: "pointer",
3621
+ outline: "none",
3622
+ boxShadow: selected ? "0 0 0 2px rgba(255,255,255,0.95) inset" : "none",
3623
+ transition: "transform 0.08s ease",
3624
+ transform: selected ? "scale(1.08)" : "scale(1)",
3625
+ WebkitTapHighlightColor: "transparent"
3626
+ }
3627
+ }
3628
+ );
3629
+ }
3630
+ function ColorPalette({ value, onChange }) {
3631
+ const [showCustom, setShowCustom] = react.useState(false);
3632
+ const normalized = normalizeHex(value);
3633
+ const inPalette = TLDRAW_PALETTE.some((c) => hexesEqual(c.hex, normalized));
3634
+ return /* @__PURE__ */ jsxRuntime.jsxs(
3635
+ "div",
3636
+ {
3637
+ style: {
3638
+ display: "grid",
3639
+ gridTemplateColumns: "repeat(6, 24px)",
3640
+ gap: 6,
3641
+ rowGap: 6,
3642
+ alignItems: "center"
3643
+ },
3644
+ children: [
3645
+ TLDRAW_PALETTE.map((c) => /* @__PURE__ */ jsxRuntime.jsx(
3646
+ ColorSwatch,
3647
+ {
3648
+ color: c,
3649
+ selected: hexesEqual(c.hex, normalized),
3650
+ onSelect: () => onChange(c.hex)
3651
+ },
3652
+ c.name
3653
+ )),
3654
+ /* @__PURE__ */ jsxRuntime.jsx(
3655
+ "button",
3656
+ {
3657
+ type: "button",
3658
+ "aria-label": "Cor personalizada",
3659
+ "aria-pressed": showCustom || !inPalette,
3660
+ onClick: () => setShowCustom((v) => !v),
3661
+ onPointerDown: (e) => {
3662
+ if (e.pointerType === "mouse") e.preventDefault();
3663
+ },
3664
+ style: {
3665
+ width: 24,
3666
+ height: 24,
3667
+ padding: 0,
3668
+ borderRadius: "50%",
3669
+ border: !inPalette || showCustom ? "2px solid #18181b" : "1px solid rgba(0,0,0,0.12)",
3670
+ background: "conic-gradient(from 0deg, #ff5757, #ffbd59, #59ff7e, #59ddff, #b259ff, #ff59c1, #ff5757)",
3671
+ cursor: "pointer",
3672
+ outline: "none",
3673
+ WebkitTapHighlightColor: "transparent"
3674
+ }
3675
+ }
3676
+ ),
3677
+ showCustom || !inPalette ? /* @__PURE__ */ jsxRuntime.jsxs(
3678
+ "label",
3679
+ {
3680
+ style: {
3681
+ gridColumn: "1 / -1",
3682
+ display: "flex",
3683
+ alignItems: "center",
3684
+ gap: 6,
3685
+ fontSize: 11,
3686
+ fontWeight: 500,
3687
+ color: "#71717a",
3688
+ textTransform: "none",
3689
+ letterSpacing: 0
3690
+ },
3691
+ children: [
3692
+ /* @__PURE__ */ jsxRuntime.jsx(
3693
+ "input",
3694
+ {
3695
+ type: "color",
3696
+ value: normalized,
3697
+ onChange: (e) => onChange(e.target.value),
3698
+ style: {
3699
+ width: 28,
3700
+ height: 28,
3701
+ padding: 0,
3702
+ border: "1px solid rgba(0,0,0,0.12)",
3703
+ borderRadius: 6,
3704
+ cursor: "pointer",
3705
+ background: "transparent"
3706
+ }
3707
+ }
3708
+ ),
3709
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: { fontVariantNumeric: "tabular-nums" }, children: normalized.toUpperCase() })
3710
+ ]
3711
+ }
3712
+ ) : null
3713
+ ]
3714
+ }
3715
+ );
3716
+ }
3717
+ function DashTabs({ value, onChange }) {
3718
+ const tabs = [
3719
+ { id: "solid", label: "Cont\xEDnuo" },
3720
+ { id: "dashed", label: "Tracejado" }
3721
+ ];
3722
+ return /* @__PURE__ */ jsxRuntime.jsx(
3723
+ "div",
3724
+ {
3725
+ role: "radiogroup",
3726
+ "aria-label": "Estilo do tra\xE7o",
3727
+ style: {
3728
+ display: "flex",
3729
+ background: "rgba(24,24,27,0.06)",
3730
+ borderRadius: 7,
3731
+ padding: 2,
3732
+ gap: 2
3733
+ },
3734
+ children: tabs.map((t) => {
3735
+ const selected = value === t.id;
3736
+ return (
3737
+ // biome-ignore lint/a11y/useSemanticElements: styled segmented control needs button, not input[type=radio]
3738
+ /* @__PURE__ */ jsxRuntime.jsxs(
3739
+ "button",
3740
+ {
3741
+ type: "button",
3742
+ role: "radio",
3743
+ "aria-checked": selected,
3744
+ onClick: () => onChange(t.id),
3745
+ onPointerDown: (e) => {
3746
+ if (e.pointerType === "mouse") e.preventDefault();
3747
+ },
3748
+ style: {
3749
+ flex: 1,
3750
+ display: "inline-flex",
3751
+ alignItems: "center",
3752
+ justifyContent: "center",
3753
+ padding: "5px 8px",
3754
+ fontSize: 11,
3755
+ fontWeight: 600,
3756
+ color: selected ? "#18181b" : "#71717a",
3757
+ background: selected ? "#fff" : "transparent",
3758
+ borderRadius: 5,
3759
+ border: "none",
3760
+ cursor: "pointer",
3761
+ boxShadow: selected ? "0 1px 2px rgba(0,0,0,0.1)" : "none",
3762
+ outline: "none",
3763
+ WebkitTapHighlightColor: "transparent"
3764
+ },
3765
+ children: [
3766
+ /* @__PURE__ */ jsxRuntime.jsx(DashTabSwatch, { dashed: t.id === "dashed" }),
3767
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: { marginLeft: 6 }, children: t.label })
3768
+ ]
3769
+ },
3770
+ t.id
3771
+ )
3772
+ );
3773
+ })
3774
+ }
3775
+ );
3776
+ }
3777
+ function DashTabSwatch({ dashed }) {
3778
+ return /* @__PURE__ */ jsxRuntime.jsxs(
3779
+ "svg",
3780
+ {
3781
+ width: 26,
3782
+ height: 6,
3783
+ "aria-hidden": true,
3784
+ style: { flexShrink: 0, display: "block" },
3785
+ children: [
3786
+ /* @__PURE__ */ jsxRuntime.jsx("title", { children: dashed ? "dashed" : "solid" }),
3787
+ /* @__PURE__ */ jsxRuntime.jsx(
3788
+ "line",
3789
+ {
3790
+ x1: 1,
3791
+ y1: 3,
3792
+ x2: 25,
3793
+ y2: 3,
3794
+ stroke: "currentColor",
3795
+ strokeWidth: 2,
3796
+ strokeLinecap: "round",
3797
+ strokeDasharray: dashed ? "4 3" : void 0
3798
+ }
3799
+ )
3800
+ ]
3801
+ }
3802
+ );
3803
+ }
3804
+ function FontSizeTabs({ value, onChange }) {
3805
+ return /* @__PURE__ */ jsxRuntime.jsx(
3806
+ "div",
3807
+ {
3808
+ role: "radiogroup",
3809
+ "aria-label": "Tamanho do texto",
3810
+ style: {
3811
+ display: "flex",
3812
+ background: "rgba(24,24,27,0.06)",
3813
+ borderRadius: 7,
3814
+ padding: 2,
3815
+ gap: 2
3816
+ },
3817
+ children: FONT_SIZE_PRESETS.map((p) => {
3818
+ const selected = Math.abs(value - p.size) < 0.5;
3819
+ return (
3820
+ // biome-ignore lint/a11y/useSemanticElements: styled segmented control needs button, not input[type=radio]
3821
+ /* @__PURE__ */ jsxRuntime.jsx(
3822
+ "button",
3823
+ {
3824
+ type: "button",
3825
+ role: "radio",
3826
+ "aria-checked": selected,
3827
+ "aria-label": `${p.label} (${p.size}px)`,
3828
+ onClick: () => onChange(p.size),
3829
+ onPointerDown: (e) => {
3830
+ if (e.pointerType === "mouse") e.preventDefault();
3831
+ },
3832
+ style: {
3833
+ flex: 1,
3834
+ padding: "5px 0",
3835
+ fontSize: p.label === "XL" ? 14 : p.label === "L" ? 13 : 12,
3836
+ fontWeight: 700,
3837
+ color: selected ? "#18181b" : "#71717a",
3838
+ background: selected ? "#fff" : "transparent",
3839
+ borderRadius: 5,
3840
+ border: "none",
3841
+ cursor: "pointer",
3842
+ boxShadow: selected ? "0 1px 2px rgba(0,0,0,0.1)" : "none",
3843
+ outline: "none",
3844
+ WebkitTapHighlightColor: "transparent",
3845
+ lineHeight: 1
3846
+ },
3847
+ children: p.label
3848
+ },
3849
+ p.label
3850
+ )
3851
+ );
3852
+ })
3853
+ }
3854
+ );
3855
+ }
3856
+ function viewModelFromActiveTool(activeToolStyle) {
3857
+ const hex = normalizeHex(activeToolStyle.stroke);
3858
+ const strokeWidth = activeToolStyle.strokeWidth;
3859
+ const strokeOpacity = activeToolStyle.strokeOpacity ?? 1;
3860
+ const strokeDash = activeToolStyle.strokeDash === "dashed" ? "dashed" : "solid";
3861
+ const textFontSize = activeToolStyle.textFontSize ?? DEFAULT_TEXT_FONT_SIZE;
3862
+ const isText = activeToolStyle.toolKind === "text";
3863
+ const isDraw = activeToolStyle.toolKind === "draw";
3864
+ const isMarker = activeToolStyle.toolKind === "marker";
3865
+ const current = {
3866
+ stroke: hex,
3867
+ strokeWidth,
3868
+ ...activeToolStyle.strokeOpacity != null ? { strokeOpacity: activeToolStyle.strokeOpacity } : {},
3869
+ ...activeToolStyle.strokeDash != null ? { strokeDash: activeToolStyle.strokeDash } : {},
3870
+ ...isText ? { textFontSize } : {}
3871
+ };
3872
+ return {
3873
+ label: activeToolStyle.label ?? "Estilo da ferramenta",
3874
+ count: 0,
3875
+ hex,
3876
+ strokeWidth,
3877
+ strokeOpacity,
3878
+ strokeDash,
3879
+ textFontSize,
3880
+ showStrokeWidth: !isText,
3881
+ showDash: isDraw,
3882
+ showFontSize: isText,
3883
+ showMarkerOpacity: isMarker,
3884
+ mixedStroke: false,
3885
+ mixedWidth: false,
3886
+ mixedDash: false,
3887
+ mixedFontSize: false,
3888
+ mixedOpacity: false,
3889
+ current
3890
+ };
3891
+ }
3892
+ function viewModelFromSelection(stylable) {
3893
+ const first = stylable[0];
3894
+ if (!first) return null;
3895
+ const hex = normalizeHex(first.stroke ?? "#1d1d1d");
3896
+ const strokeWidth = first.strokeWidth ?? 2;
3897
+ const allSameStroke = stylable.every(
3898
+ (it) => (it.stroke ?? "#1d1d1d") === (first.stroke ?? "#1d1d1d")
3899
+ );
3900
+ const allSameWidth = stylable.every(
3901
+ (it) => (it.strokeWidth ?? 2) === (first.strokeWidth ?? 2)
3902
+ );
3903
+ const draws = stylable.filter(
3904
+ (it) => it.toolKind === "draw" || it.toolKind === "pencil"
3905
+ );
3906
+ const showDash = draws.length > 0;
3907
+ const firstDraw = draws[0];
3908
+ const strokeDash = firstDraw?.strokeDash === "dashed" ? "dashed" : "solid";
3909
+ const allSameDash = draws.length === 0 || draws.every((it) => (it.strokeDash ?? "solid") === strokeDash);
3910
+ const markers = stylable.filter((it) => it.toolKind === "marker");
3911
+ const showMarkerOpacity = markers.length > 0;
3912
+ const firstMarker = markers[0];
3913
+ const strokeOpacity = firstMarker?.strokeOpacity ?? 1;
3914
+ const allSameOpacity = markers.length === 0 || markers.every(
3915
+ (it) => (it.strokeOpacity ?? 1) === (firstMarker?.strokeOpacity ?? 1)
3916
+ );
3917
+ const texts = stylable.filter((it) => it.toolKind === "text");
3918
+ const showFontSize = texts.length > 0;
3919
+ const firstText = texts[0];
3920
+ const textFontSize = firstText?.textFontSize ?? DEFAULT_TEXT_FONT_SIZE;
3921
+ const allSameFontSize = texts.length === 0 || texts.every(
3922
+ (it) => (it.textFontSize ?? DEFAULT_TEXT_FONT_SIZE) === textFontSize
3923
+ );
3924
+ const current = {
3925
+ stroke: hex,
3926
+ strokeWidth,
3927
+ ...showMarkerOpacity ? { strokeOpacity } : {},
3928
+ ...showDash ? { strokeDash } : {},
3929
+ ...showFontSize ? { textFontSize } : {}
3930
+ };
3931
+ return {
3932
+ label: "Estilo da sele\xE7\xE3o",
3933
+ count: stylable.length,
3934
+ hex,
3935
+ strokeWidth,
3936
+ strokeOpacity,
3937
+ strokeDash,
3938
+ textFontSize,
3939
+ showStrokeWidth: true,
3940
+ showDash,
3941
+ showFontSize,
3942
+ showMarkerOpacity,
3943
+ mixedStroke: !allSameStroke,
3944
+ mixedWidth: !allSameWidth,
3945
+ mixedDash: !allSameDash,
3946
+ mixedFontSize: !allSameFontSize,
3947
+ mixedOpacity: !allSameOpacity,
3948
+ current
3949
+ };
3950
+ }
3518
3951
  function VectorSelectionInspector({
3519
3952
  items: itemsProp,
3520
3953
  activeToolStyle: activeToolStyleProp,
@@ -3530,175 +3963,93 @@ function VectorSelectionInspector({
3530
3963
  const activeToolStyle = activeToolStyleProp === void 0 ? ctx?.activeToolStyle ?? null : activeToolStyleProp;
3531
3964
  const onChange = onChangeProp ?? ctx?.onSelectionStyleChange ?? null;
3532
3965
  if (!onChange) return null;
3966
+ let vm = null;
3967
+ if (activeToolStyle) {
3968
+ vm = viewModelFromActiveTool(activeToolStyle);
3969
+ } else {
3970
+ const stylable = items.filter(
3971
+ (it) => !it.locked && it.toolKind && isStylableKind(it.toolKind)
3972
+ );
3973
+ if (stylable.length === 0) return null;
3974
+ vm = viewModelFromSelection(stylable);
3975
+ }
3976
+ if (!vm) return null;
3977
+ const apply = (changes) => onChange({ ...vm.current, ...changes });
3533
3978
  const shell = {
3534
3979
  ...getBoardPositionStyle(position, inset, zIndex),
3535
3980
  ...shellLook,
3536
3981
  ...style
3537
3982
  };
3538
- if (activeToolStyle) {
3539
- const stroke2 = activeToolStyle.stroke;
3540
- const strokeWidth2 = activeToolStyle.strokeWidth;
3541
- const hex2 = normalizeHex(stroke2);
3542
- const showMarkerOpacity2 = activeToolStyle.toolKind === "marker";
3543
- const opacityPct2 = Math.round((activeToolStyle.strokeOpacity ?? 1) * 100);
3544
- return /* @__PURE__ */ jsxRuntime.jsxs(
3545
- "section",
3546
- {
3547
- "data-slot": "vector-selection-inspector",
3548
- "data-position": position,
3549
- className,
3550
- "aria-label": activeToolStyle.label ?? "Estilo da ferramenta",
3551
- style: shell,
3552
- children: [
3553
- /* @__PURE__ */ jsxRuntime.jsxs("label", { style: labelStyle2, children: [
3554
- "Cor",
3555
- /* @__PURE__ */ jsxRuntime.jsx(
3556
- "input",
3557
- {
3558
- type: "color",
3559
- value: hex2,
3560
- onChange: (e) => onChange({
3561
- stroke: e.target.value,
3562
- strokeWidth: strokeWidth2,
3563
- ...activeToolStyle.strokeOpacity != null ? { strokeOpacity: activeToolStyle.strokeOpacity } : {}
3564
- }),
3565
- style: {
3566
- width: "100%",
3567
- height: 32,
3568
- padding: 0,
3569
- border: "none",
3570
- cursor: "pointer",
3571
- background: "transparent"
3572
- }
3573
- }
3574
- )
3575
- ] }),
3576
- /* @__PURE__ */ jsxRuntime.jsxs("label", { style: labelStyle2, children: [
3577
- "Grossura",
3578
- /* @__PURE__ */ jsxRuntime.jsx(
3579
- "input",
3580
- {
3581
- type: "range",
3582
- min: 1,
3583
- max: 48,
3584
- value: strokeWidth2,
3585
- onChange: (e) => onChange({
3586
- stroke: hex2,
3587
- strokeWidth: Number(e.target.value),
3588
- ...activeToolStyle.strokeOpacity != null ? { strokeOpacity: activeToolStyle.strokeOpacity } : {}
3589
- })
3590
- }
3591
- )
3592
- ] }),
3593
- showMarkerOpacity2 && /* @__PURE__ */ jsxRuntime.jsxs("label", { style: labelStyle2, children: [
3594
- "Opacidade (marcador)",
3595
- /* @__PURE__ */ jsxRuntime.jsx(
3596
- "input",
3597
- {
3598
- type: "range",
3599
- min: 10,
3600
- max: 100,
3601
- value: opacityPct2,
3602
- onChange: (e) => {
3603
- const v = Number(e.target.value) / 100;
3604
- onChange({
3605
- stroke: hex2,
3606
- strokeWidth: strokeWidth2,
3607
- strokeOpacity: v
3608
- });
3609
- }
3610
- }
3611
- ),
3612
- /* @__PURE__ */ jsxRuntime.jsxs("span", { style: { fontWeight: 500, color: "#71717a" }, children: [
3613
- opacityPct2,
3614
- "%"
3615
- ] })
3616
- ] })
3617
- ]
3618
- }
3619
- );
3620
- }
3621
- const stylable = items.filter(
3622
- (it) => !it.locked && it.toolKind && isStylableKind(it.toolKind)
3623
- );
3624
- if (stylable.length === 0) return null;
3625
- const first = stylable[0];
3626
- if (!first) return null;
3627
- const allSameStroke = stylable.every(
3628
- (it) => (it.stroke ?? "#2563eb") === (first.stroke ?? "#2563eb")
3629
- );
3630
- const allSameWidth = stylable.every(
3631
- (it) => (it.strokeWidth ?? 2) === (first.strokeWidth ?? 2)
3632
- );
3633
- const stroke = first.stroke ?? "#2563eb";
3634
- const strokeWidth = first.strokeWidth ?? 2;
3635
- const hex = normalizeHex(stroke);
3636
- const markers = stylable.filter((it) => it.toolKind === "marker");
3637
- const showMarkerOpacity = markers.length > 0;
3638
- const firstMarker = markers[0];
3639
- const allSameMarkerOpacity = markers.length > 0 && markers.every(
3640
- (it) => (it.strokeOpacity ?? 1) === (firstMarker?.strokeOpacity ?? 1)
3641
- );
3642
- const opacityPct = firstMarker ? Math.round((firstMarker.strokeOpacity ?? 1) * 100) : 100;
3983
+ const opacityPct = Math.round(vm.strokeOpacity * 100);
3643
3984
  return /* @__PURE__ */ jsxRuntime.jsxs(
3644
3985
  "section",
3645
3986
  {
3646
3987
  "data-slot": "vector-selection-inspector",
3647
3988
  "data-position": position,
3648
3989
  className,
3649
- "aria-label": "Estilo da sele\xE7\xE3o",
3990
+ "aria-label": vm.label,
3650
3991
  style: shell,
3651
3992
  children: [
3652
- stylable.length > 1 && /* @__PURE__ */ jsxRuntime.jsxs("p", { style: { margin: 0, fontSize: 11, color: "#71717a" }, children: [
3653
- stylable.length,
3993
+ vm.count > 1 ? /* @__PURE__ */ jsxRuntime.jsxs("p", { style: { margin: 0, fontSize: 11, color: "#71717a" }, children: [
3994
+ vm.count,
3654
3995
  " objetos selecionados"
3655
- ] }),
3656
- /* @__PURE__ */ jsxRuntime.jsxs("label", { style: labelStyle2, children: [
3657
- "Cor",
3658
- !allSameStroke && /* @__PURE__ */ jsxRuntime.jsxs("span", { style: { fontWeight: 400, color: "#a1a1aa" }, children: [
3659
- " ",
3660
- "(valores misturados \u2014 novo valor aplica a todos)"
3996
+ ] }) : null,
3997
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { role: "group", "aria-label": "Cor", style: sectionLabelStyle, children: [
3998
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { children: [
3999
+ "Cor",
4000
+ vm.mixedStroke ? /* @__PURE__ */ jsxRuntime.jsx("span", { style: mixedHintStyle, children: " (valores misturados)" }) : null
3661
4001
  ] }),
3662
- /* @__PURE__ */ jsxRuntime.jsx(
3663
- "input",
3664
- {
3665
- type: "color",
3666
- value: hex,
3667
- onChange: (e) => onChange({
3668
- stroke: e.target.value,
3669
- strokeWidth
3670
- }),
3671
- style: {
3672
- width: "100%",
3673
- height: 32,
3674
- padding: 0,
3675
- border: "none",
3676
- cursor: "pointer",
3677
- background: "transparent"
3678
- }
3679
- }
3680
- )
4002
+ /* @__PURE__ */ jsxRuntime.jsx(ColorPalette, { value: vm.hex, onChange: (h) => apply({ stroke: h }) })
3681
4003
  ] }),
3682
- /* @__PURE__ */ jsxRuntime.jsxs("label", { style: labelStyle2, children: [
4004
+ vm.showStrokeWidth ? /* @__PURE__ */ jsxRuntime.jsxs("label", { style: sectionLabelStyle, children: [
3683
4005
  "Grossura",
3684
- !allSameWidth && /* @__PURE__ */ jsxRuntime.jsx("span", { style: { fontWeight: 400, color: "#a1a1aa" }, children: " (misturado)" }),
4006
+ vm.mixedWidth ? /* @__PURE__ */ jsxRuntime.jsx("span", { style: mixedHintStyle, children: " (misturado)" }) : null,
3685
4007
  /* @__PURE__ */ jsxRuntime.jsx(
3686
4008
  "input",
3687
4009
  {
3688
4010
  type: "range",
3689
4011
  min: 1,
3690
4012
  max: 48,
3691
- value: strokeWidth,
3692
- onChange: (e) => onChange({
3693
- stroke: hex,
3694
- strokeWidth: Number(e.target.value)
3695
- })
4013
+ value: vm.strokeWidth,
4014
+ onChange: (e) => apply({ strokeWidth: Number(e.target.value) })
3696
4015
  }
3697
4016
  )
3698
- ] }),
3699
- showMarkerOpacity && firstMarker && /* @__PURE__ */ jsxRuntime.jsxs("label", { style: labelStyle2, children: [
3700
- "Opacidade (marcador)",
3701
- !allSameMarkerOpacity && markers.length > 1 && /* @__PURE__ */ jsxRuntime.jsx("span", { style: { fontWeight: 400, color: "#a1a1aa" }, children: " (misturado)" }),
4017
+ ] }) : null,
4018
+ vm.showDash ? (
4019
+ // biome-ignore lint/a11y/useSemanticElements: fieldset would impose default browser styling that breaks the inspector layout
4020
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { role: "group", "aria-label": "Tra\xE7o", style: sectionLabelStyle, children: [
4021
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { children: [
4022
+ "Tra\xE7o",
4023
+ vm.mixedDash ? /* @__PURE__ */ jsxRuntime.jsx("span", { style: mixedHintStyle, children: " (misturado)" }) : null
4024
+ ] }),
4025
+ /* @__PURE__ */ jsxRuntime.jsx(
4026
+ DashTabs,
4027
+ {
4028
+ value: vm.strokeDash,
4029
+ onChange: (next) => apply({ strokeDash: next })
4030
+ }
4031
+ )
4032
+ ] })
4033
+ ) : null,
4034
+ vm.showFontSize ? (
4035
+ // biome-ignore lint/a11y/useSemanticElements: fieldset would impose default browser styling that breaks the inspector layout
4036
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { role: "group", "aria-label": "Tamanho", style: sectionLabelStyle, children: [
4037
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { children: [
4038
+ "Tamanho",
4039
+ vm.mixedFontSize ? /* @__PURE__ */ jsxRuntime.jsx("span", { style: mixedHintStyle, children: " (misturado)" }) : null
4040
+ ] }),
4041
+ /* @__PURE__ */ jsxRuntime.jsx(
4042
+ FontSizeTabs,
4043
+ {
4044
+ value: vm.textFontSize,
4045
+ onChange: (size) => apply({ textFontSize: size })
4046
+ }
4047
+ )
4048
+ ] })
4049
+ ) : null,
4050
+ vm.showMarkerOpacity ? /* @__PURE__ */ jsxRuntime.jsxs("label", { style: sectionLabelStyle, children: [
4051
+ "Opacidade",
4052
+ vm.mixedOpacity ? /* @__PURE__ */ jsxRuntime.jsx("span", { style: mixedHintStyle, children: " (misturado)" }) : null,
3702
4053
  /* @__PURE__ */ jsxRuntime.jsx(
3703
4054
  "input",
3704
4055
  {
@@ -3706,21 +4057,14 @@ function VectorSelectionInspector({
3706
4057
  min: 10,
3707
4058
  max: 100,
3708
4059
  value: opacityPct,
3709
- onChange: (e) => {
3710
- const v = Number(e.target.value) / 100;
3711
- onChange({
3712
- stroke: hex,
3713
- strokeWidth,
3714
- strokeOpacity: v
3715
- });
3716
- }
4060
+ onChange: (e) => apply({ strokeOpacity: Number(e.target.value) / 100 })
3717
4061
  }
3718
4062
  ),
3719
4063
  /* @__PURE__ */ jsxRuntime.jsxs("span", { style: { fontWeight: 500, color: "#71717a" }, children: [
3720
4064
  opacityPct,
3721
4065
  "%"
3722
4066
  ] })
3723
- ] })
4067
+ ] }) : null
3724
4068
  ]
3725
4069
  }
3726
4070
  );
@@ -4199,6 +4543,24 @@ function splitToolbarTools(tools, overflowIds) {
4199
4543
  const overflow = overflowIds.map((id) => tools.find((t) => t.id === id)).filter((t) => Boolean(t));
4200
4544
  return { primary, overflow };
4201
4545
  }
4546
+ function usePromotedOverflowTool({
4547
+ overflowTools,
4548
+ selectedId,
4549
+ initialPromotedId
4550
+ }) {
4551
+ const [lastOverflowToolId, setLastOverflowToolId] = react.useState(() => {
4552
+ if (initialPromotedId == null) return null;
4553
+ return overflowTools.some((t) => t.id === initialPromotedId) ? initialPromotedId : null;
4554
+ });
4555
+ react.useEffect(() => {
4556
+ if (overflowTools.some((t) => t.id === selectedId)) {
4557
+ setLastOverflowToolId(selectedId);
4558
+ }
4559
+ }, [selectedId, overflowTools]);
4560
+ const promotedTool = lastOverflowToolId ? overflowTools.find((t) => t.id === lastOverflowToolId) : void 0;
4561
+ const remainingOverflowTools = promotedTool ? overflowTools.filter((t) => t.id !== promotedTool.id) : overflowTools;
4562
+ return { promotedTool, remainingOverflowTools };
4563
+ }
4202
4564
  var icOverflow = { size: 18, strokeWidth: 2 };
4203
4565
  function useOverflowDropdown() {
4204
4566
  const [open, setOpen] = react.useState(false);
@@ -4733,6 +5095,15 @@ function VectorToolbarComponent({
4733
5095
  const pluginContext = react.useContext(CanvuPluginContext);
4734
5096
  const runtimeTools = pluginContext?.resolvedTools;
4735
5097
  const resolvedTools = tools ?? runtimeTools ?? DEFAULT_VECTOR_TOOLS;
5098
+ const { primary: primaryTools, overflow: overflowTools } = splitToolbarTools(
5099
+ resolvedTools,
5100
+ overflowToolIds
5101
+ );
5102
+ const { promotedTool, remainingOverflowTools } = usePromotedOverflowTool({
5103
+ overflowTools,
5104
+ selectedId: value,
5105
+ initialPromotedId: overflowToolIds[0] ?? null
5106
+ });
4736
5107
  if (typeof children === "function") {
4737
5108
  const ctx = {
4738
5109
  tools: resolvedTools,
@@ -4777,11 +5148,8 @@ function VectorToolbarComponent({
4777
5148
  )
4778
5149
  ] });
4779
5150
  }
4780
- const { primary: primaryTools, overflow: overflowTools } = splitToolbarTools(
4781
- resolvedTools,
4782
- overflowToolIds
4783
- );
4784
5151
  const showOverflowMenu = overflowTools.length > 0;
5152
+ const showOverflowDropdown = remainingOverflowTools.length > 0;
4785
5153
  const inlineCtx = {
4786
5154
  selectedId: value,
4787
5155
  selectTool: onChange,
@@ -4832,6 +5200,7 @@ function VectorToolbarComponent({
4832
5200
  /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": true, style: toolLockDividerStyle })
4833
5201
  ] }) : null,
4834
5202
  primaryTools.map((tool) => renderInlineToolButton(tool, inlineCtx)),
5203
+ promotedTool ? renderInlineToolButton(promotedTool, inlineCtx) : null,
4835
5204
  showOverflowMenu && orientation === "horizontal" ? /* @__PURE__ */ jsxRuntime.jsx(
4836
5205
  "span",
4837
5206
  {
@@ -4840,10 +5209,10 @@ function VectorToolbarComponent({
4840
5209
  style: overflowSpacerStyle
4841
5210
  }
4842
5211
  ) : null,
4843
- showOverflowMenu ? /* @__PURE__ */ jsxRuntime.jsx(
5212
+ showOverflowDropdown ? /* @__PURE__ */ jsxRuntime.jsx(
4844
5213
  ToolbarOverflowMenu,
4845
5214
  {
4846
- tools: overflowTools,
5215
+ tools: remainingOverflowTools,
4847
5216
  selectedId: value,
4848
5217
  onSelect: onChange,
4849
5218
  density,
@@ -6494,6 +6863,7 @@ var VectorScene = class {
6494
6863
 
6495
6864
  // src/react/VectorViewport.tsx
6496
6865
  init_shape_builders();
6866
+ init_text_svg();
6497
6867
 
6498
6868
  // src/react/blob-url-lifecycle.ts
6499
6869
  var SVG_BLOB_HREF_PATTERN = /\b(?:href|xlink:href)=["'](blob:[^"']+)["']/g;
@@ -6699,6 +7069,9 @@ function InteractionOverlay({
6699
7069
  );
6700
7070
  } else if (placementPreview.kind === "rect" || placementPreview.kind === "ellipse" || placementPreview.kind === "architectural-cloud") {
6701
7071
  const r = normalizeRect(placementPreview.rect);
7072
+ const shapeStroke = previewStrokeStyle?.stroke ?? "#1d1d1d";
7073
+ const shapeWidth = previewStrokeStyle?.strokeWidth ?? 2;
7074
+ const shapeOpacity = previewStrokeStyle?.strokeOpacity;
6702
7075
  preview = placementPreview.kind === "rect" ? /* @__PURE__ */ jsxRuntime.jsx(
6703
7076
  "rect",
6704
7077
  {
@@ -6706,11 +7079,11 @@ function InteractionOverlay({
6706
7079
  y: r.y,
6707
7080
  width: r.width,
6708
7081
  height: r.height,
7082
+ rx: 4,
6709
7083
  fill: "none",
6710
- stroke: "#64748b",
6711
- strokeWidth: overlayStrokePx,
6712
- strokeDasharray: dashPattern,
6713
- vectorEffect: "non-scaling-stroke"
7084
+ stroke: shapeStroke,
7085
+ strokeWidth: shapeWidth,
7086
+ strokeOpacity: shapeOpacity
6714
7087
  }
6715
7088
  ) : placementPreview.kind === "ellipse" ? /* @__PURE__ */ jsxRuntime.jsx(
6716
7089
  "ellipse",
@@ -6720,29 +7093,30 @@ function InteractionOverlay({
6720
7093
  rx: r.width / 2,
6721
7094
  ry: r.height / 2,
6722
7095
  fill: "none",
6723
- stroke: "#64748b",
6724
- strokeWidth: overlayStrokePx,
6725
- strokeDasharray: dashPattern,
6726
- vectorEffect: "non-scaling-stroke"
7096
+ stroke: shapeStroke,
7097
+ strokeWidth: shapeWidth,
7098
+ strokeOpacity: shapeOpacity
6727
7099
  }
6728
7100
  ) : /* @__PURE__ */ jsxRuntime.jsx("g", { transform: `translate(${r.x}, ${r.y})`, children: /* @__PURE__ */ jsxRuntime.jsx(
6729
7101
  "path",
6730
7102
  {
6731
- d: buildArchitecturalCloudPathD(r.width, r.height, overlayStrokePx),
7103
+ d: buildArchitecturalCloudPathD(r.width, r.height, shapeWidth),
6732
7104
  fill: "none",
6733
- stroke: "#64748b",
6734
- strokeWidth: overlayStrokePx,
6735
- strokeDasharray: dashPattern,
7105
+ stroke: shapeStroke,
7106
+ strokeWidth: shapeWidth,
7107
+ strokeOpacity: shapeOpacity,
6736
7108
  strokeLinecap: "round",
6737
- strokeLinejoin: "round",
6738
- vectorEffect: "non-scaling-stroke"
7109
+ strokeLinejoin: "round"
6739
7110
  }
6740
7111
  ) });
6741
7112
  } else if (placementPreview.kind === "line" || placementPreview.kind === "arrow") {
6742
7113
  const { start, end } = placementPreview;
7114
+ const shapeStroke = previewStrokeStyle?.stroke ?? "#1d1d1d";
7115
+ const shapeWidth = previewStrokeStyle?.strokeWidth ?? 2;
7116
+ const shapeOpacity = previewStrokeStyle?.strokeOpacity;
6743
7117
  const geometry = placementPreview.kind === "arrow" ? computeStraightArrowGeometry(
6744
7118
  { x1: start.x, y1: start.y, x2: end.x, y2: end.y },
6745
- overlayStrokePx
7119
+ shapeWidth
6746
7120
  ) : null;
6747
7121
  preview = /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
6748
7122
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -6752,11 +7126,10 @@ function InteractionOverlay({
6752
7126
  y1: start.y,
6753
7127
  x2: geometry?.shaftEndX ?? end.x,
6754
7128
  y2: geometry?.shaftEndY ?? end.y,
6755
- stroke: "#64748b",
6756
- strokeWidth: overlayStrokePx,
6757
- strokeDasharray: dashPattern,
6758
- strokeLinecap: "round",
6759
- vectorEffect: "non-scaling-stroke"
7129
+ stroke: shapeStroke,
7130
+ strokeWidth: shapeWidth,
7131
+ strokeOpacity: shapeOpacity,
7132
+ strokeLinecap: "round"
6760
7133
  }
6761
7134
  ),
6762
7135
  placementPreview.kind === "arrow" && geometry ? /* @__PURE__ */ jsxRuntime.jsx(
@@ -6764,10 +7137,9 @@ function InteractionOverlay({
6764
7137
  {
6765
7138
  d: `M ${geometry.headLeftX} ${geometry.headLeftY} L ${geometry.headTipX} ${geometry.headTipY} L ${geometry.headRightX} ${geometry.headRightY}`,
6766
7139
  fill: "none",
6767
- stroke: "#64748b",
6768
- strokeWidth: overlayStrokePx,
6769
- strokeOpacity: 0.9,
6770
- strokeDasharray: dashPattern,
7140
+ stroke: shapeStroke,
7141
+ strokeWidth: shapeWidth,
7142
+ strokeOpacity: shapeOpacity,
6771
7143
  strokeLinecap: "round",
6772
7144
  strokeLinejoin: "round"
6773
7145
  }
@@ -6781,7 +7153,8 @@ function InteractionOverlay({
6781
7153
  const previewStyle = {
6782
7154
  stroke: isLaser ? "#f43f5e" : previewStrokeStyle?.stroke ?? "#64748b",
6783
7155
  strokeWidth: isLaser ? 4 : previewStrokeStyle?.strokeWidth ?? (tool === "marker" ? 16 : 3),
6784
- ...previewStrokeStyle?.strokeOpacity != null && !isLaser ? { strokeOpacity: previewStrokeStyle.strokeOpacity } : {}
7156
+ ...previewStrokeStyle?.strokeOpacity != null && !isLaser ? { strokeOpacity: previewStrokeStyle.strokeOpacity } : {},
7157
+ ...previewStrokeStyle?.strokeDash != null && !isLaser ? { strokeDash: previewStrokeStyle.strokeDash } : {}
6785
7158
  };
6786
7159
  const payload = computeFreehandSvgPayload(
6787
7160
  raw,
@@ -6825,6 +7198,7 @@ function InteractionOverlay({
6825
7198
  strokeOpacity: isLaser ? 0.85 : payload.strokeOpacity,
6826
7199
  strokeLinecap: "round",
6827
7200
  strokeLinejoin: "round",
7201
+ strokeDasharray: payload.strokeDasharray,
6828
7202
  shapeRendering: "geometricPrecision"
6829
7203
  }
6830
7204
  );
@@ -7230,6 +7604,9 @@ function TextEditOverlay({
7230
7604
  const fontSize = fsWorld * z;
7231
7605
  const lineHeight = fsWorld * (22 / 18) * z;
7232
7606
  const fixedBox = !!item.textFixedBounds;
7607
+ const padTop = EDIT_TOP_PAD_RATIO * fsWorld * z;
7608
+ const padX = 4 * z;
7609
+ const textColor = item.stroke ?? "#1d1d1d";
7233
7610
  return /* @__PURE__ */ jsxRuntime.jsx(
7234
7611
  "div",
7235
7612
  {
@@ -7260,14 +7637,14 @@ function TextEditOverlay({
7260
7637
  width: "100%",
7261
7638
  height: "100%",
7262
7639
  margin: 0,
7263
- padding: 2,
7640
+ padding: `${padTop}px ${padX}px 0px ${padX}px`,
7264
7641
  border: "none",
7265
7642
  borderRadius: 0,
7266
7643
  resize: "none",
7267
- fontFamily: "system-ui, sans-serif",
7644
+ fontFamily: TEXT_FONT_FAMILY,
7268
7645
  fontSize,
7269
7646
  lineHeight: `${lineHeight}px`,
7270
- color: "#0f172a",
7647
+ color: textColor,
7271
7648
  background: "transparent",
7272
7649
  backgroundColor: "transparent",
7273
7650
  outline: "none",
@@ -7275,7 +7652,6 @@ function TextEditOverlay({
7275
7652
  appearance: "none",
7276
7653
  WebkitAppearance: "none",
7277
7654
  WebkitTapHighlightColor: "transparent",
7278
- /* Auto-sized text: no soft wrap / scrollbar; fixed box: wrap like the SVG. */
7279
7655
  whiteSpace: fixedBox ? "pre-wrap" : "pre",
7280
7656
  overflow: fixedBox ? "auto" : "hidden",
7281
7657
  overflowX: fixedBox ? "hidden" : "hidden",
@@ -7965,14 +8341,32 @@ var VectorViewport = react.forwardRef(
7965
8341
  imageStoreRef.current = new IndexedDbImageStore();
7966
8342
  }
7967
8343
  const rememberedImageBlobHrefsRef = react.useRef(/* @__PURE__ */ new Set());
7968
- const strokeStyleRef = react.useRef({ ...DEFAULT_STROKE_STYLE });
8344
+ const strokeStyleRef = react.useRef({
8345
+ ...DEFAULT_STROKE_STYLE,
8346
+ textFontSize: DEFAULT_TEXT_FONT_SIZE
8347
+ });
7969
8348
  const [strokeStyleState, setStrokeStyleState] = react.useState({
7970
- ...DEFAULT_STROKE_STYLE
8349
+ ...DEFAULT_STROKE_STYLE,
8350
+ textFontSize: DEFAULT_TEXT_FONT_SIZE
7971
8351
  });
7972
8352
  const setCurrentStrokeStyle = react.useCallback((next) => {
7973
8353
  strokeStyleRef.current = next;
7974
8354
  setStrokeStyleState(next);
7975
8355
  }, []);
8356
+ const patchCurrentStrokeStyle = react.useCallback(
8357
+ (patch) => {
8358
+ const merged = { ...strokeStyleRef.current };
8359
+ for (const key of Object.keys(patch)) {
8360
+ const v = patch[key];
8361
+ if (v !== void 0) {
8362
+ merged[key] = v;
8363
+ }
8364
+ }
8365
+ strokeStyleRef.current = merged;
8366
+ setStrokeStyleState(merged);
8367
+ },
8368
+ []
8369
+ );
7976
8370
  const progressiveQueueRef = react.useRef(/* @__PURE__ */ new Set());
7977
8371
  react.useEffect(() => {
7978
8372
  const store = imageStoreRef.current;
@@ -8121,10 +8515,11 @@ var VectorViewport = react.forwardRef(
8121
8515
  change(
8122
8516
  exists ? replaceItem(itemsRef.current, id, item) : [...itemsRef.current, item]
8123
8517
  );
8124
- setCurrentStrokeStyle({
8518
+ patchCurrentStrokeStyle({
8125
8519
  stroke: item.stroke ?? DEFAULT_STROKE_STYLE.stroke,
8126
8520
  strokeWidth: item.strokeWidth ?? DEFAULT_STROKE_STYLE.strokeWidth,
8127
- ...item.strokeOpacity != null ? { strokeOpacity: item.strokeOpacity } : {}
8521
+ strokeOpacity: item.strokeOpacity,
8522
+ strokeDash: item.strokeDash
8128
8523
  });
8129
8524
  }
8130
8525
  if (!shouldKeepToolForContinuousPenInput(args.tool, args.pointerType)) {
@@ -8132,8 +8527,8 @@ var VectorViewport = react.forwardRef(
8132
8527
  }
8133
8528
  },
8134
8529
  [
8530
+ patchCurrentStrokeStyle,
8135
8531
  requestAutoResetTool,
8136
- setCurrentStrokeStyle,
8137
8532
  shouldKeepToolForContinuousPenInput
8138
8533
  ]
8139
8534
  );
@@ -8869,24 +9264,28 @@ var VectorViewport = react.forwardRef(
8869
9264
  renderFrame();
8870
9265
  }, [renderFrame]);
8871
9266
  react.useEffect(() => {
9267
+ const current = strokeStyleRef.current;
8872
9268
  if (toolId === "marker") {
8873
9269
  setCurrentStrokeStyle({
8874
- ...strokeStyleRef.current,
9270
+ ...current,
8875
9271
  ...MARKER_TOOL_STYLE
8876
9272
  });
8877
9273
  return;
8878
9274
  }
8879
- const current = strokeStyleRef.current;
8880
9275
  if (isDefaultMarkerToolStyle(current)) {
8881
9276
  setCurrentStrokeStyle({
8882
9277
  stroke: DEFAULT_STROKE_STYLE.stroke,
8883
- strokeWidth: toolId === "draw" ? 10 : DEFAULT_STROKE_STYLE.strokeWidth
9278
+ strokeWidth: toolId === "draw" ? 10 : DEFAULT_STROKE_STYLE.strokeWidth,
9279
+ strokeDash: current.strokeDash,
9280
+ textFontSize: current.textFontSize
8884
9281
  });
8885
9282
  return;
8886
9283
  }
8887
9284
  setCurrentStrokeStyle({
8888
9285
  stroke: current.stroke,
8889
- strokeWidth: toolId === "draw" ? 10 : current.strokeWidth
9286
+ strokeWidth: toolId === "draw" ? 10 : current.strokeWidth,
9287
+ strokeDash: current.strokeDash,
9288
+ textFontSize: current.textFontSize
8890
9289
  });
8891
9290
  }, [setCurrentStrokeStyle, toolId]);
8892
9291
  react.useEffect(() => {
@@ -8895,15 +9294,14 @@ var VectorViewport = react.forwardRef(
8895
9294
  const it = items.find((i) => i.id === primaryId);
8896
9295
  if (!it || it.locked) return;
8897
9296
  if (it.toolKind === "image") return;
8898
- const next = {
9297
+ patchCurrentStrokeStyle({
8899
9298
  stroke: it.stroke ?? DEFAULT_STROKE_STYLE.stroke,
8900
- strokeWidth: it.strokeWidth ?? DEFAULT_STROKE_STYLE.strokeWidth
8901
- };
8902
- if (it.strokeOpacity != null) {
8903
- next.strokeOpacity = it.strokeOpacity;
8904
- }
8905
- setCurrentStrokeStyle(next);
8906
- }, [effectiveSelectedIds, items, setCurrentStrokeStyle]);
9299
+ strokeWidth: it.strokeWidth ?? DEFAULT_STROKE_STYLE.strokeWidth,
9300
+ strokeOpacity: it.strokeOpacity,
9301
+ strokeDash: it.strokeDash,
9302
+ ...it.toolKind === "text" && it.textFontSize != null ? { textFontSize: it.textFontSize } : {}
9303
+ });
9304
+ }, [effectiveSelectedIds, items, patchCurrentStrokeStyle]);
8907
9305
  const handleSelectionStyleChange = react.useCallback(
8908
9306
  (patch) => {
8909
9307
  const change = onItemsChangeRef.current;
@@ -8919,25 +9317,15 @@ var VectorViewport = react.forwardRef(
8919
9317
  nextList = replaceItem(nextList, id, out);
8920
9318
  }
8921
9319
  change(nextList);
8922
- setCurrentStrokeStyle({
8923
- ...strokeStyleRef.current,
8924
- stroke: patch.stroke,
8925
- strokeWidth: patch.strokeWidth,
8926
- ...patch.strokeOpacity != null ? { strokeOpacity: patch.strokeOpacity } : {}
8927
- });
9320
+ patchCurrentStrokeStyle(patch);
8928
9321
  },
8929
- [setCurrentStrokeStyle]
9322
+ [patchCurrentStrokeStyle]
8930
9323
  );
8931
9324
  const handleActiveToolStyleChange = react.useCallback(
8932
9325
  (patch) => {
8933
- const current = strokeStyleRef.current;
8934
- setCurrentStrokeStyle({
8935
- stroke: patch.stroke,
8936
- strokeWidth: patch.strokeWidth,
8937
- ...patch.strokeOpacity != null ? { strokeOpacity: patch.strokeOpacity } : current.strokeOpacity != null ? { strokeOpacity: current.strokeOpacity } : {}
8938
- });
9326
+ patchCurrentStrokeStyle(patch);
8939
9327
  },
8940
- [setCurrentStrokeStyle]
9328
+ [patchCurrentStrokeStyle]
8941
9329
  );
8942
9330
  const commitTextEdit = react.useCallback(() => {
8943
9331
  const id = editingTextIdRef.current;
@@ -10070,16 +10458,21 @@ var VectorViewport = react.forwardRef(
10070
10458
  const id = createShapeId();
10071
10459
  const { x: worldX, y: worldY } = st.startWorld;
10072
10460
  if (st.tool === "text") {
10461
+ const fs = strokeStyleRef.current.textFontSize;
10462
+ const baseline = textBaselineYFor(fs);
10463
+ const lh = textLineHeightFor(fs);
10464
+ const minH = Math.max(26, baseline + Math.max(4, lh * 0.2));
10073
10465
  const newItem = createTextItem(
10074
10466
  id,
10075
10467
  {
10076
10468
  x: worldX - 4,
10077
- y: worldY - 24,
10469
+ y: worldY - baseline,
10078
10470
  width: 160,
10079
- height: 36
10471
+ height: minH
10080
10472
  },
10081
10473
  "",
10082
- strokeStyleRef.current
10474
+ strokeStyleRef.current,
10475
+ fs
10083
10476
  );
10084
10477
  editingTextSnapshotRef.current = {
10085
10478
  ...newItem,
@@ -10230,12 +10623,63 @@ var VectorViewport = react.forwardRef(
10230
10623
  return effectiveSelectedIds.map((id) => resolvedItems.find((i) => i.id === id)).filter((i) => i != null);
10231
10624
  }, [effectiveSelectedIds, resolvedItems]);
10232
10625
  const activeToolInspectorStyle = react.useMemo(() => {
10233
- if (toolId !== "draw") return void 0;
10234
- return {
10235
- toolKind: "draw",
10236
- label: "Estilo da ferramenta",
10237
- ...strokeStyleState
10238
- };
10626
+ if (toolId === "draw") {
10627
+ return {
10628
+ toolKind: "draw",
10629
+ label: "Caneta",
10630
+ ...strokeStyleState
10631
+ };
10632
+ }
10633
+ if (toolId === "marker") {
10634
+ return {
10635
+ toolKind: "marker",
10636
+ label: "Marcador",
10637
+ ...strokeStyleState
10638
+ };
10639
+ }
10640
+ if (toolId === "text") {
10641
+ return {
10642
+ toolKind: "text",
10643
+ label: "Texto",
10644
+ ...strokeStyleState
10645
+ };
10646
+ }
10647
+ if (toolId === "rect") {
10648
+ return {
10649
+ toolKind: "rect",
10650
+ label: "Ret\xE2ngulo",
10651
+ ...strokeStyleState
10652
+ };
10653
+ }
10654
+ if (toolId === "ellipse") {
10655
+ return {
10656
+ toolKind: "ellipse",
10657
+ label: "Elipse",
10658
+ ...strokeStyleState
10659
+ };
10660
+ }
10661
+ if (toolId === "architectural-cloud") {
10662
+ return {
10663
+ toolKind: "architectural-cloud",
10664
+ label: "Nuvem",
10665
+ ...strokeStyleState
10666
+ };
10667
+ }
10668
+ if (toolId === "line") {
10669
+ return {
10670
+ toolKind: "line",
10671
+ label: "Linha",
10672
+ ...strokeStyleState
10673
+ };
10674
+ }
10675
+ if (toolId === "arrow") {
10676
+ return {
10677
+ toolKind: "arrow",
10678
+ label: "Seta",
10679
+ ...strokeStyleState
10680
+ };
10681
+ }
10682
+ return void 0;
10239
10683
  }, [strokeStyleState, toolId]);
10240
10684
  const eraserPreviewItemsForOverlay = react.useMemo(() => {
10241
10685
  return eraserPreviewIds.map((id) => resolvedItems.find((i) => i.id === id)).filter((i) => i != null);