@zuilib/text-editor 0.6.0 → 0.7.1

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/CHANGELOG.md CHANGED
@@ -1,5 +1,36 @@
1
1
  # Changelog — @zuilib/text-editor
2
2
 
3
+ ## 0.7.1
4
+
5
+ - Fix: free `text` shapes could not be typed into. The text tool's editor
6
+ opened on pointerdown and was blurred by the same click; it now opens on
7
+ pointerup like the card editors
8
+ - Fix: selecting a shape no longer shifts the drawing. The properties row
9
+ is always rendered (showing the defaults for the next shape when nothing
10
+ is selected) with a fixed minimum height, so click-to-edit lands where
11
+ the pointer is
12
+ - Fix: keyboard shortcuts keep working after committing text (the canvas
13
+ regains focus); `Home`/`End` work inside the inline text editor
14
+
15
+ ## 0.7.0
16
+
17
+ **Themed drawing canvas.** The canvas chrome now follows the host theme
18
+ instead of shipping its own look.
19
+
20
+ - Tool row and contextual property row are a flat header in the editor's
21
+ own toolbar idiom (in flow, no floating panels over the drawing)
22
+ - Every color, radius, shadow and font of the canvas is a
23
+ `--zui-drawing-*` custom property defaulting to the core theme tokens
24
+ (`--primary`, `--foreground`, `--border`, `--popover`, `--radius`,
25
+ `--shadow-medium`, …), so light and dark mode come from the tokens;
26
+ override them on `.zui-drawing-canvas` or an ancestor. See README,
27
+ "Theming the canvas"
28
+ - Selection frame, handles, marquee, bind highlight and connector label
29
+ plates are drawn in `currentColor`/theme variables; the table selection
30
+ tint uses `--primary` too, so the editor shares one accent
31
+ - `--zui-drawing-dark-filter` exposes the dark-mode inversion of the
32
+ drawing area (`none` opts out)
33
+
3
34
  ## 0.6.0
4
35
 
5
36
  **Text measure and `text` block width.** Additive: markdown, JSON, class
package/README.md CHANGED
@@ -353,6 +353,41 @@ The full skeleton spec (attach sides, explicit positions, free texts) is
353
353
  in [DRAWING_FORMAT.md](./DRAWING_FORMAT.md). Programmatic access:
354
354
  `parseDrawingSkeleton`, `expandSkeleton`, `DRAWING_SKELETON_JSON_SCHEMA`.
355
355
 
356
+ ### Theming the canvas
357
+
358
+ The canvas chrome (header, property row, popover, selection handles) is
359
+ styled entirely through `--zui-drawing-*` custom properties. They default
360
+ to the ZUI core tokens (`--primary`, `--foreground`, `--border`,
361
+ `--popover`, `--radius`, `--shadow-medium`, …), so the canvas follows the
362
+ host theme in light and dark mode. Override any of them on
363
+ `.zui-drawing-canvas` or an ancestor:
364
+
365
+ ```css
366
+ .my-app .zui-drawing-canvas {
367
+ --zui-drawing-accent: #0f766e; /* selection, active tool, handles */
368
+ --zui-drawing-surface: #fbfbf7; /* drawing area (pre dark-mode filter) */
369
+ --zui-drawing-grid: rgba(0, 0, 0, 0.12);
370
+ --zui-drawing-grid-size: 16px;
371
+ --zui-drawing-font: 'Inter', sans-serif;
372
+ }
373
+ ```
374
+
375
+ | Variable | Default | Used for |
376
+ |----------|---------|----------|
377
+ | `--zui-drawing-accent` | `var(--primary)` | Selection frame and handles, active tool, focus ring, bind highlight |
378
+ | `--zui-drawing-foreground` | `var(--foreground)` | Header text and icons |
379
+ | `--zui-drawing-muted-foreground` | `var(--muted-foreground)` | Swatch labels, empty-state hint |
380
+ | `--zui-drawing-border` | `var(--border)` | Block border, header divider, popover border |
381
+ | `--zui-drawing-chrome` | `var(--background)` | Header and property row background |
382
+ | `--zui-drawing-popover` / `-foreground` | `var(--popover)` / `var(--popover-foreground)` | "More shapes" menu |
383
+ | `--zui-drawing-danger` / `--zui-drawing-success` | `var(--destructive)` / `var(--success)` | Delete button, "copied" state |
384
+ | `--zui-drawing-radius` / `--zui-drawing-control-radius` | `var(--radius-lg)` / `var(--radius)` | Block corners / buttons and menus |
385
+ | `--zui-drawing-shadow` | `var(--shadow-medium)` | Popover |
386
+ | `--zui-drawing-font` | system sans | Shape text and the inline editor |
387
+ | `--zui-drawing-surface` / `--zui-drawing-grid` / `--zui-drawing-grid-size` | white / 9% black / `20px` | Drawing area and its dot grid, before the dark filter |
388
+ | `--zui-drawing-dark-filter` | `invert(93%) hue-rotate(180deg)` | Applied to the drawing area under `.dark`; set to `none` to opt out |
389
+ | `--zui-drawing-hover` / `--zui-drawing-active` | `currentColor` 8% / 12% | Button hover and pressed backgrounds |
390
+
356
391
  ## Outline & section folding
357
392
 
358
393
  `outline` docks a collapsible table-of-contents sidebar: live heading list
package/dist/index.js CHANGED
@@ -1652,7 +1652,6 @@ function PropertyBar({
1652
1652
 
1653
1653
  // src/drawing/canvas/SelectionOverlay.tsx
1654
1654
  import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
1655
- var SELECTION_COLOR = "#6965db";
1656
1655
  var HANDLE = 7;
1657
1656
  function Handle({
1658
1657
  x,
@@ -1664,8 +1663,6 @@ function Handle({
1664
1663
  }) {
1665
1664
  const props = {
1666
1665
  className: "zui-drawing-handle",
1667
- fill: "#fff",
1668
- stroke: SELECTION_COLOR,
1669
1666
  strokeWidth: 1.5,
1670
1667
  style: { cursor },
1671
1668
  onPointerDown
@@ -1694,12 +1691,10 @@ function SelectionOverlay({
1694
1691
  ghosts.map(({ index, x, y }) => /* @__PURE__ */ jsx3(
1695
1692
  "circle",
1696
1693
  {
1697
- className: "zui-drawing-ghost",
1694
+ className: "zui-drawing-handle zui-drawing-ghost",
1698
1695
  cx: x,
1699
1696
  cy: y,
1700
1697
  r: 4.5,
1701
- fill: "#fff",
1702
- stroke: SELECTION_COLOR,
1703
1698
  strokeWidth: 1.5,
1704
1699
  strokeDasharray: "2 2",
1705
1700
  style: { cursor: "copy" },
@@ -1795,7 +1790,7 @@ function SelectionFrame({ rect, pad }) {
1795
1790
  height: rect.h + pad * 2,
1796
1791
  rx: 6,
1797
1792
  fill: "none",
1798
- stroke: SELECTION_COLOR,
1793
+ stroke: "currentColor",
1799
1794
  strokeWidth: 1,
1800
1795
  strokeDasharray: "5 4",
1801
1796
  pointerEvents: "none"
@@ -1826,7 +1821,7 @@ function GroupSelectionOverlay({
1826
1821
  height: r.h + 6,
1827
1822
  rx: 4,
1828
1823
  fill: "none",
1829
- stroke: SELECTION_COLOR,
1824
+ stroke: "currentColor",
1830
1825
  strokeWidth: 1,
1831
1826
  opacity: 0.45
1832
1827
  },
@@ -1839,14 +1834,15 @@ function MarqueeOverlay({ rect }) {
1839
1834
  return /* @__PURE__ */ jsx3(
1840
1835
  "rect",
1841
1836
  {
1837
+ className: "zui-drawing-selection",
1842
1838
  x: rect.x,
1843
1839
  y: rect.y,
1844
1840
  width: rect.w,
1845
1841
  height: rect.h,
1846
1842
  rx: 3,
1847
- fill: SELECTION_COLOR,
1843
+ fill: "currentColor",
1848
1844
  fillOpacity: 0.08,
1849
- stroke: SELECTION_COLOR,
1845
+ stroke: "currentColor",
1850
1846
  strokeWidth: 1,
1851
1847
  pointerEvents: "none"
1852
1848
  }
@@ -2000,7 +1996,6 @@ function BoxGeometry({
2000
1996
  // src/drawing/canvas/ShapeView.tsx
2001
1997
  import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
2002
1998
  import { createElement } from "react";
2003
- var FONT_FAMILY = "ui-sans-serif, system-ui, sans-serif";
2004
1999
  var CONNECTOR_FONT_SIZE = 12;
2005
2000
  var CONNECTOR_LABEL_MAX_WIDTH = 160;
2006
2001
  function slotLayout(shape) {
@@ -2036,7 +2031,6 @@ function BoxTexts({
2036
2031
  const mainStart = area.y + area.h / 2 - (mainLines.length - 1) * mainLineH / 2 + FONT_SIZE * 0.35;
2037
2032
  const common = {
2038
2033
  fill: shape.stroke,
2039
- fontFamily: FONT_FAMILY,
2040
2034
  textAnchor: "middle"
2041
2035
  };
2042
2036
  return /* @__PURE__ */ jsxs5("g", { children: [
@@ -2134,7 +2128,7 @@ function ConnectorLabel({
2134
2128
  width: size.w + 10,
2135
2129
  height: size.h + 6,
2136
2130
  rx: 5,
2137
- fill: "#ffffff",
2131
+ className: "zui-drawing-label-plate",
2138
2132
  opacity: 0.94
2139
2133
  }
2140
2134
  ),
@@ -2145,7 +2139,6 @@ function ConnectorLabel({
2145
2139
  y: startY + i * lineH,
2146
2140
  fill: shape.stroke,
2147
2141
  fontSize: CONNECTOR_FONT_SIZE,
2148
- fontFamily: FONT_FAMILY,
2149
2142
  textAnchor: "middle",
2150
2143
  style: textStyle,
2151
2144
  children: line
@@ -2190,7 +2183,6 @@ function ShapeView({
2190
2183
  y: shape.y + FONT_SIZE,
2191
2184
  fill: shape.stroke,
2192
2185
  fontSize: FONT_SIZE,
2193
- fontFamily: FONT_FAMILY,
2194
2186
  style: { userSelect: "none", whiteSpace: "pre" },
2195
2187
  children: (shape.text ?? "").split("\n").map((line, i) => /* @__PURE__ */ jsx5("tspan", { x: shape.x, dy: i === 0 ? 0 : FONT_SIZE * LINE_HEIGHT, children: line }, i))
2196
2188
  }
@@ -2242,6 +2234,14 @@ function TextEditOverlay({
2242
2234
  el.select();
2243
2235
  const onKeyDown = (e) => {
2244
2236
  e.stopPropagation();
2237
+ if ((e.key === "Home" || e.key === "End") && !e.shiftKey && !e.metaKey && !e.ctrlKey) {
2238
+ e.preventDefault();
2239
+ const text = el.value;
2240
+ const caret = e.key === "Home" ? el.selectionStart : el.selectionEnd;
2241
+ const pos = e.key === "Home" ? text.lastIndexOf("\n", caret - 1) + 1 : (text.indexOf("\n", caret) + 1 || text.length + 1) - 1;
2242
+ el.setSelectionRange(pos, pos);
2243
+ return;
2244
+ }
2245
2245
  if (e.key === "Enter" && !e.shiftKey) {
2246
2246
  e.preventDefault();
2247
2247
  onCommit(shape.id, field, valueRef.current);
@@ -2262,7 +2262,7 @@ function TextEditOverlay({
2262
2262
  }, []);
2263
2263
  const style = {
2264
2264
  color: shape.stroke,
2265
- fontFamily: FONT_FAMILY,
2265
+ fontFamily: "var(--zui-drawing-font)",
2266
2266
  lineHeight: LINE_HEIGHT
2267
2267
  };
2268
2268
  if (shape.type === "text") {
@@ -2273,7 +2273,9 @@ function TextEditOverlay({
2273
2273
  fontSize: FONT_SIZE,
2274
2274
  width: Math.max(80, size.w + 20),
2275
2275
  height: size.h + 2,
2276
- whiteSpace: "pre"
2276
+ // `pre` would keep the lines, but Chrome ignores Home/End in a
2277
+ // `white-space: pre` textarea; the width tracks the content anyway
2278
+ whiteSpace: "pre-wrap"
2277
2279
  });
2278
2280
  } else if (isConnectorType(shape.type)) {
2279
2281
  const lines = wrapText(value, CONNECTOR_LABEL_MAX_WIDTH, CONNECTOR_FONT_SIZE);
@@ -2545,6 +2547,7 @@ function DrawingCanvas({ nodeKey, data }) {
2545
2547
  const rootRef = useRef4(null);
2546
2548
  const svgRef = useRef4(null);
2547
2549
  const dragRef = useRef4(null);
2550
+ const pendingTextEditRef = useRef4(null);
2548
2551
  const pendingPointRef = useRef4(null);
2549
2552
  const frameRef = useRef4(null);
2550
2553
  const lastCommittedRef = useRef4(serializeDrawingData(data));
@@ -2671,7 +2674,8 @@ function DrawingCanvas({ nodeKey, data }) {
2671
2674
  }
2672
2675
  ]);
2673
2676
  setTool("select");
2674
- startTextEditing(id2, "text");
2677
+ setSelectedIds(/* @__PURE__ */ new Set([id2]));
2678
+ pendingTextEditRef.current = id2;
2675
2679
  return;
2676
2680
  }
2677
2681
  const id = createShapeId();
@@ -2775,6 +2779,12 @@ function DrawingCanvas({ nodeKey, data }) {
2775
2779
  }
2776
2780
  pendingPointRef.current = null;
2777
2781
  setHoverBoxId(null);
2782
+ if (pendingTextEditRef.current) {
2783
+ const id = pendingTextEditRef.current;
2784
+ pendingTextEditRef.current = null;
2785
+ startTextEditing(id, "text");
2786
+ return;
2787
+ }
2778
2788
  if (!drag) return;
2779
2789
  const point = getPoint(e);
2780
2790
  if (drag.mode === "marquee") {
@@ -2958,6 +2968,7 @@ function DrawingCanvas({ nodeKey, data }) {
2958
2968
  const commitText = useCallback(
2959
2969
  (id, field, value) => {
2960
2970
  setEditingText(null);
2971
+ rootRef.current?.focus({ preventScroll: true });
2961
2972
  updateShapes(
2962
2973
  (prev) => prev.flatMap((s) => {
2963
2974
  if (s.id !== id) return [s];
@@ -2982,7 +2993,6 @@ function DrawingCanvas({ nodeKey, data }) {
2982
2993
  }, []);
2983
2994
  const editingShape = shapes.find((s) => s.id === editingText?.id) ?? null;
2984
2995
  const hoverBox = hoverBoxId ? shapes.find((s) => s.id === hoverBoxId) : null;
2985
- const showProps = isEditable && !editingText && (selection.length > 0 || tool !== "select");
2986
2996
  const propStroke = single?.stroke ?? selection[0]?.stroke ?? stroke;
2987
2997
  const propFill = single?.fill ?? selection.find((s) => isBoxType(s.type))?.fill ?? fill;
2988
2998
  const canvasCursor = tool === "select" ? "default" : tool === "text" ? "text" : "crosshair";
@@ -3012,7 +3022,7 @@ function DrawingCanvas({ nodeKey, data }) {
3012
3022
  onCopyMermaid: copyMermaid
3013
3023
  }
3014
3024
  ),
3015
- /* @__PURE__ */ jsx9("div", { className: `zui-drawing-props-host ${showProps ? "is-visible" : ""}`, children: /* @__PURE__ */ jsx9(
3025
+ /* @__PURE__ */ jsx9("div", { className: "zui-drawing-props-host", children: /* @__PURE__ */ jsx9(
3016
3026
  PropertyBar,
3017
3027
  {
3018
3028
  selection,
@@ -3027,118 +3037,120 @@ function DrawingCanvas({ nodeKey, data }) {
3027
3037
  }
3028
3038
  ) })
3029
3039
  ] }),
3030
- /* @__PURE__ */ jsxs8(
3031
- "svg",
3032
- {
3033
- ref: svgRef,
3034
- className: "zui-drawing-surface",
3035
- style: {
3036
- height: logicalWidth ? void 0 : canvasHeight,
3037
- width: logicalWidth ?? void 0,
3038
- maxWidth: "100%",
3039
- aspectRatio: logicalWidth ? `${logicalWidth} / ${canvasHeight}` : void 0,
3040
- cursor: canvasCursor
3041
- },
3042
- viewBox: logicalWidth ? `0 0 ${logicalWidth} ${canvasHeight}` : void 0,
3043
- preserveAspectRatio: "xMinYMin meet",
3044
- onPointerDown: handleBackgroundPointerDown,
3045
- onPointerMove: handlePointerMove,
3046
- onPointerUp: handlePointerUp,
3047
- onPointerCancel: handlePointerUp,
3048
- onDoubleClick: (e) => {
3049
- if (!isEditable) return;
3050
- const target = e.target.closest("[data-shape-id]");
3051
- const id = target?.getAttribute("data-shape-id");
3052
- const shape = id ? shapes.find((s) => s.id === id) : null;
3053
- if (!shape) return;
3054
- if (shape.type === "text" || isConnectorType(shape.type)) {
3055
- startTextEditing(shape.id, "text");
3056
- } else {
3057
- startTextEditing(shape.id, slotAt(shape, getPoint(e).y));
3058
- }
3059
- },
3060
- children: [
3061
- shapes.map((shape) => /* @__PURE__ */ jsxs8(
3062
- "g",
3063
- {
3064
- "data-shape-id": shape.id,
3065
- className: `zui-drawing-shape ${selectedIds.has(shape.id) ? "is-selected" : ""}`,
3066
- style: { cursor: isEditable && tool === "select" ? "move" : void 0 },
3067
- onPointerDown: (e) => handleShapePointerDown(e, shape),
3068
- children: [
3069
- /* @__PURE__ */ jsx9(HitArea, { shape, points: paths.get(shape.id) }),
3070
- shape.id === editingText?.id && shape.type === "text" ? null : /* @__PURE__ */ jsx9(
3071
- ShapeView,
3072
- {
3073
- shape,
3074
- points: paths.get(shape.id),
3075
- hideField: shape.id === editingText?.id ? editingText.field : null,
3076
- showHints: isEditable && tool === "select" && single?.id === shape.id && shape.id !== editingText?.id
3077
- }
3078
- )
3079
- ]
3080
- },
3081
- shape.id
3082
- )),
3083
- hoverBox && /* @__PURE__ */ jsx9(
3084
- "polygon",
3085
- {
3086
- className: "zui-drawing-bind-target",
3087
- points: boxOutline(hoverBox).map((p) => `${p.x},${p.y}`).join(" "),
3088
- fill: SELECTION_COLOR,
3089
- fillOpacity: 0.06,
3090
- stroke: SELECTION_COLOR,
3091
- strokeWidth: 2,
3092
- strokeLinejoin: "round",
3093
- pointerEvents: "none"
3094
- }
3095
- ),
3096
- single && isEditable && !editingText && /* @__PURE__ */ jsx9(
3097
- SelectionOverlay,
3098
- {
3099
- shape: single,
3100
- points: paths.get(single.id) ?? [],
3101
- onHandlePointerDown: handleHandlePointerDown,
3102
- onWaypointAdd: addWaypoint,
3103
- onWaypointRemove: removeWaypoint
3040
+ /* @__PURE__ */ jsxs8("div", { className: "zui-drawing-stage", children: [
3041
+ /* @__PURE__ */ jsxs8(
3042
+ "svg",
3043
+ {
3044
+ ref: svgRef,
3045
+ className: "zui-drawing-surface",
3046
+ style: {
3047
+ height: logicalWidth ? void 0 : canvasHeight,
3048
+ width: logicalWidth ?? void 0,
3049
+ maxWidth: "100%",
3050
+ aspectRatio: logicalWidth ? `${logicalWidth} / ${canvasHeight}` : void 0,
3051
+ cursor: canvasCursor
3052
+ },
3053
+ viewBox: logicalWidth ? `0 0 ${logicalWidth} ${canvasHeight}` : void 0,
3054
+ preserveAspectRatio: "xMinYMin meet",
3055
+ onPointerDown: handleBackgroundPointerDown,
3056
+ onPointerMove: handlePointerMove,
3057
+ onPointerUp: handlePointerUp,
3058
+ onPointerCancel: handlePointerUp,
3059
+ onDoubleClick: (e) => {
3060
+ if (!isEditable) return;
3061
+ const target = e.target.closest("[data-shape-id]");
3062
+ const id = target?.getAttribute("data-shape-id");
3063
+ const shape = id ? shapes.find((s) => s.id === id) : null;
3064
+ if (!shape) return;
3065
+ if (shape.type === "text" || isConnectorType(shape.type)) {
3066
+ startTextEditing(shape.id, "text");
3067
+ } else {
3068
+ startTextEditing(shape.id, slotAt(shape, getPoint(e).y));
3104
3069
  }
3105
- ),
3106
- selection.length > 1 && isEditable && /* @__PURE__ */ jsx9(GroupSelectionOverlay, { shapes: selection, paths }),
3107
- marquee && /* @__PURE__ */ jsx9(MarqueeOverlay, { rect: marqueeRect(marquee.origin, marquee.current) })
3108
- ]
3109
- }
3110
- ),
3111
- isEditable && shapes.length === 0 && /* @__PURE__ */ jsx9("div", { className: "zui-drawing-empty", "aria-hidden": "true", children: "Pick a shape above, then click or drag on the canvas" }),
3112
- editingShape && editingText && /* @__PURE__ */ jsx9(
3113
- "div",
3114
- {
3115
- className: "zui-drawing-overlay",
3116
- style: {
3117
- transform: `scale(${scale})`,
3118
- width: logicalWidth ?? void 0,
3119
- height: logicalWidth ? canvasHeight : void 0
3120
- },
3121
- children: /* @__PURE__ */ jsx9(
3122
- TextEditOverlay,
3123
- {
3124
- shape: editingShape,
3125
- field: editingText.field,
3126
- points: paths.get(editingShape.id),
3127
- onCommit: commitText
3128
3070
  },
3129
- `${editingShape.id}:${editingText.field}`
3130
- )
3131
- }
3132
- ),
3133
- isEditable && /* @__PURE__ */ jsx9(
3134
- HeightHandle,
3135
- {
3136
- height: canvasHeight,
3137
- scale,
3138
- onChange: setCanvasHeight,
3139
- onCommit: (h) => commit(shapesRef.current, { height: h })
3140
- }
3141
- )
3071
+ children: [
3072
+ shapes.map((shape) => /* @__PURE__ */ jsxs8(
3073
+ "g",
3074
+ {
3075
+ "data-shape-id": shape.id,
3076
+ className: `zui-drawing-shape ${selectedIds.has(shape.id) ? "is-selected" : ""}`,
3077
+ style: { cursor: isEditable && tool === "select" ? "move" : void 0 },
3078
+ onPointerDown: (e) => handleShapePointerDown(e, shape),
3079
+ children: [
3080
+ /* @__PURE__ */ jsx9(HitArea, { shape, points: paths.get(shape.id) }),
3081
+ shape.id === editingText?.id && shape.type === "text" ? null : /* @__PURE__ */ jsx9(
3082
+ ShapeView,
3083
+ {
3084
+ shape,
3085
+ points: paths.get(shape.id),
3086
+ hideField: shape.id === editingText?.id ? editingText.field : null,
3087
+ showHints: isEditable && tool === "select" && single?.id === shape.id && shape.id !== editingText?.id
3088
+ }
3089
+ )
3090
+ ]
3091
+ },
3092
+ shape.id
3093
+ )),
3094
+ hoverBox && /* @__PURE__ */ jsx9(
3095
+ "polygon",
3096
+ {
3097
+ className: "zui-drawing-selection zui-drawing-bind-target",
3098
+ points: boxOutline(hoverBox).map((p) => `${p.x},${p.y}`).join(" "),
3099
+ fill: "currentColor",
3100
+ fillOpacity: 0.06,
3101
+ stroke: "currentColor",
3102
+ strokeWidth: 2,
3103
+ strokeLinejoin: "round",
3104
+ pointerEvents: "none"
3105
+ }
3106
+ ),
3107
+ single && isEditable && !editingText && /* @__PURE__ */ jsx9(
3108
+ SelectionOverlay,
3109
+ {
3110
+ shape: single,
3111
+ points: paths.get(single.id) ?? [],
3112
+ onHandlePointerDown: handleHandlePointerDown,
3113
+ onWaypointAdd: addWaypoint,
3114
+ onWaypointRemove: removeWaypoint
3115
+ }
3116
+ ),
3117
+ selection.length > 1 && isEditable && /* @__PURE__ */ jsx9(GroupSelectionOverlay, { shapes: selection, paths }),
3118
+ marquee && /* @__PURE__ */ jsx9(MarqueeOverlay, { rect: marqueeRect(marquee.origin, marquee.current) })
3119
+ ]
3120
+ }
3121
+ ),
3122
+ isEditable && shapes.length === 0 && /* @__PURE__ */ jsx9("div", { className: "zui-drawing-empty", "aria-hidden": "true", children: "Pick a shape above, then click or drag on the canvas" }),
3123
+ editingShape && editingText && /* @__PURE__ */ jsx9(
3124
+ "div",
3125
+ {
3126
+ className: "zui-drawing-overlay",
3127
+ style: {
3128
+ transform: `scale(${scale})`,
3129
+ width: logicalWidth ?? void 0,
3130
+ height: logicalWidth ? canvasHeight : void 0
3131
+ },
3132
+ children: /* @__PURE__ */ jsx9(
3133
+ TextEditOverlay,
3134
+ {
3135
+ shape: editingShape,
3136
+ field: editingText.field,
3137
+ points: paths.get(editingShape.id),
3138
+ onCommit: commitText
3139
+ },
3140
+ `${editingShape.id}:${editingText.field}`
3141
+ )
3142
+ }
3143
+ ),
3144
+ isEditable && /* @__PURE__ */ jsx9(
3145
+ HeightHandle,
3146
+ {
3147
+ height: canvasHeight,
3148
+ scale,
3149
+ onChange: setCanvasHeight,
3150
+ onCommit: (h) => commit(shapesRef.current, { height: h })
3151
+ }
3152
+ )
3153
+ ] })
3142
3154
  ]
3143
3155
  }
3144
3156
  );
package/dist/styles.css CHANGED
@@ -362,7 +362,7 @@
362
362
  .zui-table-selected,
363
363
  .zui-table-cell.zui-table-selected {
364
364
  outline: none;
365
- background: color-mix(in srgb, #6965db 14%, transparent);
365
+ background: color-mix(in srgb, var(--primary, #6965db) 14%, transparent);
366
366
  }
367
367
 
368
368
  /*
@@ -391,24 +391,57 @@
391
391
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.4);
392
392
  }
393
393
 
394
- /* Drawing canvas */
394
+ /*
395
+ * Drawing canvas.
396
+ *
397
+ * Every color, radius and shadow of the canvas chrome comes from a
398
+ * `--zui-drawing-*` custom property. They default to the ZUI core theme
399
+ * tokens (`--primary`, `--foreground`, `--border`, …) so the canvas follows
400
+ * the host theme, light and dark, and consumers override them on
401
+ * `.zui-drawing-canvas` (or any ancestor) to restyle without touching
402
+ * selectors. `--zui-drawing-surface` and `--zui-drawing-grid` are the
403
+ * pre-filter colors of the drawing area: in dark mode the whole area is
404
+ * passed through `--zui-drawing-dark-filter` so stored shape colors stay
405
+ * theme-agnostic (set the filter to `none` to opt out).
406
+ */
395
407
  .zui-drawing {
396
408
  margin-block: 1rem;
397
409
  }
398
410
 
399
411
  .zui-drawing-canvas {
400
- --zui-drawing-accent: #6965db;
412
+ --zui-drawing-accent: var(--primary, #3b82f6);
413
+ --zui-drawing-foreground: var(--foreground, #0a0a0a);
414
+ --zui-drawing-muted-foreground: var(--muted-foreground, #71717a);
415
+ --zui-drawing-border: var(--border, #e4e4e7);
416
+ --zui-drawing-chrome: var(--background, #ffffff);
417
+ --zui-drawing-popover: var(--popover, #ffffff);
418
+ --zui-drawing-popover-foreground: var(--popover-foreground, var(--zui-drawing-foreground));
419
+ --zui-drawing-danger: var(--destructive, #ef4444);
420
+ --zui-drawing-success: var(--success, #22c55e);
421
+ --zui-drawing-radius: var(--radius-lg, 0.75rem);
422
+ --zui-drawing-control-radius: var(--radius, 0.375rem);
423
+ --zui-drawing-shadow: var(--shadow-medium, 0 4px 6px -1px rgba(0, 0, 0, 0.1));
424
+ --zui-drawing-font: ui-sans-serif, system-ui, sans-serif;
425
+ --zui-drawing-surface: #ffffff;
426
+ --zui-drawing-grid: rgba(0, 0, 0, 0.09);
427
+ --zui-drawing-grid-size: 20px;
428
+ --zui-drawing-dark-filter: invert(93%) hue-rotate(180deg);
429
+ --zui-drawing-hover: color-mix(in srgb, currentColor 8%, transparent);
430
+ --zui-drawing-active: color-mix(in srgb, currentColor 12%, transparent);
431
+
401
432
  position: relative;
402
- border: 1px solid color-mix(in srgb, currentColor 10%, transparent);
403
- border-radius: 0.875rem;
404
- background: #ffffff;
433
+ border: 1px solid var(--zui-drawing-border);
434
+ border-radius: var(--zui-drawing-radius);
435
+ background: var(--zui-drawing-chrome);
436
+ color: var(--zui-drawing-foreground);
437
+ font-family: var(--zui-drawing-font);
405
438
  outline: none;
406
439
  overflow: hidden;
407
- transition: box-shadow 0.18s ease, border-color 0.18s ease;
440
+ transition: box-shadow 0.15s ease, border-color 0.15s ease;
408
441
  }
409
442
 
410
443
  .zui-drawing-canvas.is-editable:focus-within {
411
- border-color: color-mix(in srgb, var(--zui-drawing-accent) 45%, transparent);
444
+ border-color: color-mix(in srgb, var(--zui-drawing-accent) 50%, var(--zui-drawing-border));
412
445
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--zui-drawing-accent) 12%, transparent);
413
446
  }
414
447
 
@@ -417,25 +450,30 @@
417
450
  max-width: 100%;
418
451
  }
419
452
 
453
+ .zui-drawing-stage {
454
+ position: relative;
455
+ }
456
+
420
457
  .zui-drawing-surface {
421
458
  display: block;
422
459
  width: 100%;
423
460
  max-width: 100%;
424
461
  touch-action: none;
425
- background-color: #ffffff;
426
- background-image: radial-gradient(circle, rgba(0, 0, 0, 0.09) 1px, transparent 1px);
427
- background-size: 20px 20px;
428
- }
429
-
430
- .zui-drawing-shape {
431
- transition: filter 0.12s ease;
462
+ font-family: var(--zui-drawing-font);
463
+ background-color: var(--zui-drawing-surface);
464
+ background-image: radial-gradient(circle, var(--zui-drawing-grid) 1px, transparent 1px);
465
+ background-size: var(--zui-drawing-grid-size) var(--zui-drawing-grid-size);
432
466
  }
433
467
 
434
- .zui-drawing-canvas.is-editable .zui-drawing-shape:hover:not(.is-selected) {
435
- filter: drop-shadow(0 0 2.5px color-mix(in srgb, var(--zui-drawing-accent) 55%, transparent));
468
+ /* Selection chrome inside the SVG: everything is drawn in `currentColor` */
469
+ .zui-drawing-selection,
470
+ .zui-drawing-bind-target {
471
+ color: var(--zui-drawing-accent);
436
472
  }
437
473
 
438
474
  .zui-drawing-handle {
475
+ fill: var(--zui-drawing-surface);
476
+ stroke: currentColor;
439
477
  filter: drop-shadow(0 1px 1.5px rgba(0, 0, 0, 0.25));
440
478
  }
441
479
 
@@ -448,6 +486,18 @@
448
486
  opacity: 1;
449
487
  }
450
488
 
489
+ .zui-drawing-label-plate {
490
+ fill: var(--zui-drawing-surface);
491
+ }
492
+
493
+ .zui-drawing-shape {
494
+ transition: filter 0.12s ease;
495
+ }
496
+
497
+ .zui-drawing-canvas.is-editable .zui-drawing-shape:hover:not(.is-selected) {
498
+ filter: drop-shadow(0 0 2.5px color-mix(in srgb, var(--zui-drawing-accent) 55%, transparent));
499
+ }
500
+
451
501
  .zui-drawing-bind-target {
452
502
  animation: zui-drawing-pulse 0.9s ease-in-out infinite alternate;
453
503
  }
@@ -464,43 +514,29 @@
464
514
  @keyframes zui-drawing-pop {
465
515
  from {
466
516
  opacity: 0;
467
- transform: translateY(4px) scale(0.98);
517
+ transform: translateY(-3px);
468
518
  }
469
519
  to {
470
520
  opacity: 1;
471
- transform: translateY(0) scale(1);
521
+ transform: translateY(0);
472
522
  }
473
523
  }
474
524
 
475
- /* Floating toolbar */
525
+ /* Header: tools row (+ contextual properties row), same idiom as the editor toolbar */
476
526
  .zui-drawing-toolbar {
477
- position: absolute;
478
- top: 0.625rem;
479
- left: 0.625rem;
480
- right: 0.625rem;
481
- z-index: 5;
482
527
  display: flex;
483
528
  flex-wrap: wrap;
484
- align-items: flex-start;
485
- gap: 0.5rem;
486
- pointer-events: none;
529
+ align-items: center;
530
+ gap: 0.125rem 0.5rem;
531
+ padding: 0.375rem 0.625rem;
532
+ border-bottom: 1px solid var(--zui-drawing-border);
533
+ background: var(--zui-drawing-chrome);
487
534
  }
488
535
 
489
- .zui-drawing-toolbar-group,
490
- .zui-drawing-props {
536
+ .zui-drawing-toolbar-group {
491
537
  display: flex;
492
538
  align-items: center;
493
539
  gap: 0.125rem;
494
- padding: 0.25rem;
495
- border: 1px solid rgba(0, 0, 0, 0.07);
496
- border-radius: 0.625rem;
497
- background: rgba(255, 255, 255, 0.88);
498
- backdrop-filter: blur(10px);
499
- -webkit-backdrop-filter: blur(10px);
500
- box-shadow:
501
- 0 1px 2px rgba(0, 0, 0, 0.05),
502
- 0 6px 20px -8px rgba(0, 0, 0, 0.18);
503
- pointer-events: auto;
504
540
  }
505
541
 
506
542
  .zui-drawing-toolbar-group-end {
@@ -510,8 +546,8 @@
510
546
  .zui-drawing-toolbar-divider {
511
547
  width: 1px;
512
548
  height: 1.125rem;
513
- margin: 0 0.25rem;
514
- background: rgba(0, 0, 0, 0.1);
549
+ margin: 0 0.375rem;
550
+ background: color-mix(in srgb, currentColor 15%, transparent);
515
551
  }
516
552
 
517
553
  .zui-drawing-tool {
@@ -519,57 +555,56 @@
519
555
  display: inline-flex;
520
556
  align-items: center;
521
557
  justify-content: center;
522
- width: 1.875rem;
523
- height: 1.875rem;
558
+ width: 1.75rem;
559
+ height: 1.75rem;
524
560
  border: none;
525
- border-radius: 0.5rem;
561
+ border-radius: var(--zui-drawing-control-radius);
526
562
  background: transparent;
527
- color: #1e1e1e;
563
+ color: inherit;
564
+ opacity: 0.65;
528
565
  cursor: pointer;
529
566
  transition:
530
567
  background-color 0.12s ease,
531
- color 0.12s ease,
532
- transform 0.12s ease;
568
+ opacity 0.12s ease;
533
569
  }
534
570
 
535
571
  .zui-drawing-tool:hover {
536
- background: rgba(0, 0, 0, 0.06);
537
- }
538
-
539
- .zui-drawing-tool:active {
540
- transform: scale(0.94);
572
+ opacity: 1;
573
+ background: var(--zui-drawing-hover);
541
574
  }
542
575
 
543
576
  .zui-drawing-tool.is-active {
544
- background: color-mix(in srgb, var(--zui-drawing-accent) 16%, transparent);
545
- color: #4a47b1;
577
+ opacity: 1;
578
+ color: var(--zui-drawing-accent);
579
+ background: color-mix(in srgb, var(--zui-drawing-accent) 14%, transparent);
546
580
  }
547
581
 
548
582
  .zui-drawing-tool.is-success {
549
- color: #2f9e44;
583
+ opacity: 1;
584
+ color: var(--zui-drawing-success);
550
585
  }
551
586
 
552
587
  .zui-drawing-tool-danger {
553
- color: #e03131;
588
+ color: var(--zui-drawing-danger);
554
589
  }
555
590
 
556
591
  .zui-drawing-tool-danger:hover {
557
- background: rgba(224, 49, 49, 0.1);
592
+ background: color-mix(in srgb, var(--zui-drawing-danger) 12%, transparent);
558
593
  }
559
594
 
560
595
  .zui-drawing-tool-more {
561
- width: 2.375rem;
596
+ width: 2.25rem;
562
597
  }
563
598
 
564
599
  .zui-drawing-caret {
565
600
  position: absolute;
566
- right: 0.3rem;
567
- bottom: 0.3rem;
601
+ right: 0.25rem;
602
+ bottom: 0.25rem;
568
603
  width: 0;
569
604
  height: 0;
570
605
  border-left: 4px solid transparent;
571
606
  border-top: 4px solid currentColor;
572
- opacity: 0.6;
607
+ opacity: 0.7;
573
608
  }
574
609
 
575
610
  .zui-drawing-more {
@@ -578,7 +613,7 @@
578
613
 
579
614
  .zui-drawing-popover {
580
615
  position: absolute;
581
- top: calc(100% + 0.375rem);
616
+ top: calc(100% + 0.25rem);
582
617
  left: 0;
583
618
  z-index: 7;
584
619
  display: flex;
@@ -586,14 +621,12 @@
586
621
  gap: 0.125rem;
587
622
  min-width: 9.5rem;
588
623
  padding: 0.25rem;
589
- border: 1px solid rgba(0, 0, 0, 0.08);
590
- border-radius: 0.625rem;
591
- background: #ffffff;
592
- box-shadow:
593
- 0 1px 2px rgba(0, 0, 0, 0.06),
594
- 0 10px 28px -8px rgba(0, 0, 0, 0.25);
595
- animation: zui-drawing-pop 0.14s ease-out;
596
- transform-origin: top left;
624
+ border: 1px solid var(--zui-drawing-border);
625
+ border-radius: var(--zui-drawing-control-radius);
626
+ background: var(--zui-drawing-popover);
627
+ color: var(--zui-drawing-popover-foreground);
628
+ box-shadow: var(--zui-drawing-shadow);
629
+ animation: zui-drawing-pop 0.12s ease-out;
597
630
  }
598
631
 
599
632
  .zui-drawing-popover-item {
@@ -602,9 +635,9 @@
602
635
  gap: 0.5rem;
603
636
  padding: 0.375rem 0.5rem;
604
637
  border: none;
605
- border-radius: 0.375rem;
638
+ border-radius: var(--zui-drawing-control-radius);
606
639
  background: transparent;
607
- color: #1e1e1e;
640
+ color: inherit;
608
641
  font-size: 0.8125rem;
609
642
  text-align: left;
610
643
  cursor: pointer;
@@ -612,36 +645,32 @@
612
645
  }
613
646
 
614
647
  .zui-drawing-popover-item:hover {
615
- background: rgba(0, 0, 0, 0.06);
648
+ background: var(--zui-drawing-hover);
616
649
  }
617
650
 
618
651
  .zui-drawing-popover-item.is-active {
619
- background: color-mix(in srgb, var(--zui-drawing-accent) 16%, transparent);
620
- color: #4a47b1;
652
+ color: var(--zui-drawing-accent);
653
+ background: color-mix(in srgb, var(--zui-drawing-accent) 14%, transparent);
621
654
  }
622
655
 
623
- /* Contextual property bar: its own row under the tools */
656
+ /*
657
+ * Properties row: colors for the selection (or for the next shape when
658
+ * nothing is selected) plus contextual connector options. Always rendered
659
+ * with a fixed minimum height so the drawing never shifts under the
660
+ * pointer when the selection changes.
661
+ */
624
662
  .zui-drawing-props-host {
625
663
  flex-basis: 100%;
626
664
  display: flex;
627
- opacity: 0;
628
- transform: translateY(-4px);
629
- pointer-events: none;
630
- transition:
631
- opacity 0.16s ease,
632
- transform 0.16s ease;
633
- }
634
-
635
- .zui-drawing-props-host.is-visible {
636
- opacity: 1;
637
- transform: translateY(0);
638
- pointer-events: auto;
639
665
  }
640
666
 
641
667
  .zui-drawing-props {
668
+ display: flex;
642
669
  flex-wrap: wrap;
670
+ align-items: center;
643
671
  gap: 0.25rem;
644
- padding: 0.25rem 0.375rem;
672
+ min-height: 1.75rem;
673
+ padding: 0.125rem 0 0;
645
674
  }
646
675
 
647
676
  .zui-drawing-props-group {
@@ -651,24 +680,24 @@
651
680
  }
652
681
 
653
682
  .zui-drawing-props-group + .zui-drawing-props-group {
654
- padding-left: 0.375rem;
655
- border-left: 1px solid rgba(0, 0, 0, 0.1);
683
+ padding-left: 0.5rem;
684
+ border-left: 1px solid color-mix(in srgb, currentColor 15%, transparent);
656
685
  }
657
686
 
658
687
  .zui-drawing-swatch-label {
659
688
  margin: 0 0.25rem 0 0.125rem;
660
689
  font-size: 0.6875rem;
661
690
  letter-spacing: 0.02em;
662
- color: rgba(0, 0, 0, 0.5);
691
+ color: var(--zui-drawing-muted-foreground);
663
692
  user-select: none;
664
693
  }
665
694
 
666
695
  .zui-drawing-swatch {
667
- width: 1.125rem;
668
- height: 1.125rem;
696
+ width: 1rem;
697
+ height: 1rem;
669
698
  margin: 0 0.125rem;
670
699
  padding: 0;
671
- border: 1px solid rgba(0, 0, 0, 0.14);
700
+ border: 1px solid rgba(0, 0, 0, 0.18);
672
701
  border-radius: 999px;
673
702
  cursor: pointer;
674
703
  transition:
@@ -682,7 +711,7 @@
682
711
 
683
712
  .zui-drawing-swatch.is-active {
684
713
  box-shadow:
685
- 0 0 0 2px #ffffff,
714
+ 0 0 0 2px var(--zui-drawing-chrome),
686
715
  0 0 0 3.5px var(--zui-drawing-accent);
687
716
  }
688
717
 
@@ -703,9 +732,8 @@
703
732
  display: flex;
704
733
  align-items: center;
705
734
  justify-content: center;
706
- padding-top: 2rem;
707
735
  font-size: 0.8125rem;
708
- color: rgba(0, 0, 0, 0.4);
736
+ color: var(--zui-drawing-muted-foreground);
709
737
  pointer-events: none;
710
738
  user-select: none;
711
739
  }
@@ -728,10 +756,11 @@
728
756
  padding: 0;
729
757
  border: 0;
730
758
  border-radius: 3px;
731
- outline: 1.5px dashed var(--zui-drawing-accent, #6965db);
759
+ outline: 1.5px dashed var(--zui-drawing-accent);
732
760
  outline-offset: 3px;
733
- background: rgba(255, 255, 255, 0.85);
761
+ background: color-mix(in srgb, var(--zui-drawing-surface) 85%, transparent);
734
762
  color: inherit;
763
+ font-family: var(--zui-drawing-font);
735
764
  resize: none;
736
765
  overflow: hidden;
737
766
  box-sizing: content-box;
@@ -753,7 +782,7 @@
753
782
  justify-content: center;
754
783
  width: 1.375rem;
755
784
  height: 1.375rem;
756
- border-radius: 0.375rem;
785
+ border-radius: var(--zui-drawing-control-radius);
757
786
  color: #1e1e1e;
758
787
  opacity: 0.35;
759
788
  cursor: ns-resize;
@@ -769,70 +798,15 @@
769
798
  }
770
799
 
771
800
  /*
772
- * Dark mode (zui `.dark` class convention). The drawing surface, the color
773
- * swatches, and the inline text input are darkened with a color-inversion
774
- * filter, so stored shape colors stay theme-agnostic: the default
775
- * near-black stroke renders light on the dark canvas, and the swatches
776
- * preview exactly what will be drawn.
801
+ * Dark mode (zui `.dark` class convention). The chrome follows the theme
802
+ * tokens on its own; the drawing area, the swatches and the inline text
803
+ * input are inverted so stored shape colors stay theme-agnostic.
777
804
  */
778
- .dark .zui-drawing-canvas {
779
- background: #18181b;
780
- border-color: rgba(255, 255, 255, 0.1);
781
- }
782
-
783
805
  .dark .zui-drawing-surface,
784
806
  .dark .zui-drawing-swatch,
785
- .dark .zui-drawing-text-input {
786
- filter: invert(93%) hue-rotate(180deg);
787
- }
788
-
789
- .dark .zui-drawing-toolbar-group,
790
- .dark .zui-drawing-props,
791
- .dark .zui-drawing-popover {
792
- background: rgba(35, 35, 41, 0.9);
793
- border-color: rgba(255, 255, 255, 0.1);
794
- box-shadow:
795
- 0 1px 2px rgba(0, 0, 0, 0.3),
796
- 0 8px 24px -8px rgba(0, 0, 0, 0.6);
797
- }
798
-
799
- .dark .zui-drawing-tool,
800
- .dark .zui-drawing-popover-item,
807
+ .dark .zui-drawing-text-input,
801
808
  .dark .zui-drawing-resize {
802
- color: #e3e3e8;
803
- }
804
-
805
- .dark .zui-drawing-tool:hover,
806
- .dark .zui-drawing-popover-item:hover,
807
- .dark .zui-drawing-resize:hover {
808
- background: rgba(255, 255, 255, 0.08);
809
- }
810
-
811
- .dark .zui-drawing-tool.is-active,
812
- .dark .zui-drawing-popover-item.is-active {
813
- background: #45437a;
814
- color: #dcdaff;
815
- }
816
-
817
- .dark .zui-drawing-toolbar-divider,
818
- .dark .zui-drawing-props-group + .zui-drawing-props-group {
819
- border-color: rgba(255, 255, 255, 0.12);
820
- background-color: rgba(255, 255, 255, 0.12);
821
- }
822
-
823
- .dark .zui-drawing-props-group + .zui-drawing-props-group {
824
- background-color: transparent;
825
- }
826
-
827
- .dark .zui-drawing-swatch-label,
828
- .dark .zui-drawing-empty {
829
- color: rgba(255, 255, 255, 0.55);
830
- }
831
-
832
- .dark .zui-drawing-swatch.is-active {
833
- box-shadow:
834
- 0 0 0 2px #232329,
835
- 0 0 0 3.5px #8f8bff;
809
+ filter: var(--zui-drawing-dark-filter);
836
810
  }
837
811
 
838
812
  .zui-text-editor-textarea {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zuilib/text-editor",
3
- "version": "0.6.0",
3
+ "version": "0.7.1",
4
4
  "description": "ZUI — A markdown editor component wrapping Lexical",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",