layerchart 2.0.0-next.55 → 2.0.0-next.56

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 (54) hide show
  1. package/dist/bench/ComposableLineChart.svelte +1 -1
  2. package/dist/bench/GeoBench.svelte +1 -8
  3. package/dist/components/AnnotationRange.svelte +3 -1
  4. package/dist/components/Arc.svelte +1 -3
  5. package/dist/components/ArcLabel.svelte.test.js +7 -7
  6. package/dist/components/Bar.svelte +4 -2
  7. package/dist/components/BoxPlot.svelte +4 -12
  8. package/dist/components/Cell.svelte +13 -8
  9. package/dist/components/Chart.svelte +69 -26
  10. package/dist/components/ChartChildren.svelte +22 -4
  11. package/dist/components/Circle.svelte +34 -12
  12. package/dist/components/ClipPath.svelte +3 -9
  13. package/dist/components/Ellipse.svelte +27 -6
  14. package/dist/components/GeoLegend.svelte +1 -3
  15. package/dist/components/GeoPoint.svelte +25 -3
  16. package/dist/components/GeoSpline.svelte +1 -4
  17. package/dist/components/GeoTile.svelte +8 -4
  18. package/dist/components/Group.svelte +11 -5
  19. package/dist/components/Highlight.svelte +3 -3
  20. package/dist/components/Image.svelte +42 -30
  21. package/dist/components/Labels.svelte +2 -4
  22. package/dist/components/Line.svelte +7 -6
  23. package/dist/components/LinearGradient.svelte +8 -4
  24. package/dist/components/Link.svelte +0 -1
  25. package/dist/components/Marker.svelte +9 -1
  26. package/dist/components/Path.svelte +43 -23
  27. package/dist/components/Pattern.svelte +101 -5
  28. package/dist/components/Pattern.svelte.d.ts +3 -1
  29. package/dist/components/Pie.svelte +2 -6
  30. package/dist/components/RadialGradient.svelte +8 -4
  31. package/dist/components/Rect.svelte +29 -12
  32. package/dist/components/Spline.svelte +22 -4
  33. package/dist/components/Text.svelte +9 -5
  34. package/dist/components/Trail.svelte +19 -7
  35. package/dist/components/Vector.svelte +37 -14
  36. package/dist/components/Violin.svelte +1 -2
  37. package/dist/components/charts/ArcChart.svelte +8 -5
  38. package/dist/components/charts/AreaChart.svelte +6 -1
  39. package/dist/components/charts/BarChart.svelte +3 -1
  40. package/dist/components/charts/LineChart.svelte +6 -1
  41. package/dist/components/charts/PieChart.svelte +10 -3
  42. package/dist/components/tooltip/Tooltip.svelte +2 -8
  43. package/dist/contexts/chart.d.ts +1 -1
  44. package/dist/contexts/chart.js +3 -1
  45. package/dist/server/TestBarChart.svelte +28 -28
  46. package/dist/server/TestLineChart.svelte +28 -28
  47. package/dist/server/index.js +1 -1
  48. package/dist/states/brush.svelte.js +16 -13
  49. package/dist/states/chart.svelte.test.js +24 -19
  50. package/dist/states/geo.svelte.js +1 -4
  51. package/dist/states/series.svelte.js +1 -1
  52. package/dist/utils/canvas.js +7 -4
  53. package/dist/utils/trail.js +3 -4
  54. package/package.json +1 -1
@@ -63,11 +63,25 @@
63
63
 
64
64
  {#if layerCtx === 'svg'}
65
65
  {#if children}
66
- <Group {x} {y} opacity={opacity as number} class={className as string} {...extractLayerProps(restProps, 'lc-geo-point-group')}>
66
+ <Group
67
+ {x}
68
+ {y}
69
+ opacity={opacity as number}
70
+ class={className as string}
71
+ {...extractLayerProps(restProps, 'lc-geo-point-group')}
72
+ >
67
73
  {@render children({ x, y })}
68
74
  </Group>
69
75
  {:else}
70
- <Circle cx={x} cy={y} {opacity} {fillOpacity} {strokeWidth} class={className} {...extractLayerProps(restProps, 'lc-geo-point')} />
76
+ <Circle
77
+ cx={x}
78
+ cy={y}
79
+ {opacity}
80
+ {fillOpacity}
81
+ {strokeWidth}
82
+ class={className}
83
+ {...extractLayerProps(restProps, 'lc-geo-point')}
84
+ />
71
85
  {/if}
72
86
  {/if}
73
87
 
@@ -78,6 +92,14 @@
78
92
  {@render children({ x, y })}
79
93
  <!-- </Group> -->
80
94
  {:else}
81
- <Circle cx={x} cy={y} {opacity} {fillOpacity} {strokeWidth} class={className} {...extractLayerProps(restProps, 'lc-geo-point')} />
95
+ <Circle
96
+ cx={x}
97
+ cy={y}
98
+ {opacity}
99
+ {fillOpacity}
100
+ {strokeWidth}
101
+ class={className}
102
+ {...extractLayerProps(restProps, 'lc-geo-point')}
103
+ />
82
104
  {/if}
83
105
  {/if}
@@ -75,7 +75,4 @@
75
75
  );
76
76
  </script>
77
77
 
78
- <Path
79
- pathData={d}
80
- {...extractLayerProps(restProps, 'lc-geo-spline')}
81
- />
78
+ <Path pathData={d} {...extractLayerProps(restProps, 'lc-geo-spline')} />
@@ -91,10 +91,14 @@
91
91
  }
92
92
 
93
93
  if (layerCtx === 'canvas') {
94
- ctx.registerComponent({ name: 'GeoTile', kind: 'mark', canvasRender: {
95
- render,
96
- deps: () => [tiles],
97
- } });
94
+ ctx.registerComponent({
95
+ name: 'GeoTile',
96
+ kind: 'mark',
97
+ canvasRender: {
98
+ render,
99
+ deps: () => [tiles],
100
+ },
101
+ });
98
102
  }
99
103
  </script>
100
104
 
@@ -137,9 +137,7 @@
137
137
  const dataMode = $derived(hasAnyDataProp(x, y));
138
138
 
139
139
  // Data to iterate over in data mode
140
- const resolvedData: any[] = $derived(
141
- dataMode ? (dataProp ?? chartDataArray(chartCtx.data)) : []
142
- );
140
+ const resolvedData: any[] = $derived(dataMode ? (dataProp ?? chartDataArray(chartCtx.data)) : []);
143
141
 
144
142
  // Resolve a single data item to pixel coordinates
145
143
  function resolveGroup(d: any) {
@@ -198,10 +196,18 @@
198
196
  const initialY = initialYProp ?? (typeof y === 'number' ? y : undefined);
199
197
 
200
198
  const trueX = $derived(
201
- typeof x === 'number' ? x : (x == null && (center === 'x' || center === true) ? chartCtx.width / 2 : 0)
199
+ typeof x === 'number'
200
+ ? x
201
+ : x == null && (center === 'x' || center === true)
202
+ ? chartCtx.width / 2
203
+ : 0
202
204
  );
203
205
  const trueY = $derived(
204
- typeof y === 'number' ? y : (y == null && (center === 'y' || center === true) ? chartCtx.height / 2 : 0)
206
+ typeof y === 'number'
207
+ ? y
208
+ : y == null && (center === 'y' || center === true)
209
+ ? chartCtx.height / 2
210
+ : 0
205
211
  );
206
212
  const motionX = createMotion(initialX, () => trueX, motion);
207
213
  const motionY = createMotion(initialY, () => trueY, motion);
@@ -389,9 +389,9 @@
389
389
  : null;
390
390
  // For diverging stacks, use the outer edge (y0 for below-baseline, y1 for above)
391
391
  const stackedY1 = stackValue
392
- ? (ctx.series.stackLayout === 'stackDiverging' && stackValue[1] <= 0
393
- ? stackValue[0]
394
- : stackValue[1])
392
+ ? ctx.series.stackLayout === 'stackDiverging' && stackValue[1] <= 0
393
+ ? stackValue[0]
394
+ : stackValue[1]
395
395
  : 0;
396
396
 
397
397
  if (ctx.valueAxis === 'x') {
@@ -195,16 +195,16 @@
195
195
  const geo = getGeoContext();
196
196
 
197
197
  // Data to iterate over in data mode
198
- const resolvedData: any[] = $derived(
199
- dataMode ? (dataProp ?? chartDataArray(chartCtx.data)) : []
200
- );
198
+ const resolvedData: any[] = $derived(dataMode ? (dataProp ?? chartDataArray(chartCtx.data)) : []);
201
199
 
202
200
  // Resolve a single data item to pixel coordinates and dimensions
203
201
  function resolveImage(d: any) {
204
202
  const resolvedR = r !== undefined ? resolveDataProp(r, d, null, 0) : undefined;
205
203
  const defaultSize = resolvedR !== undefined ? resolvedR * 2 : 16;
206
- const resolvedWidth = width !== undefined ? resolveDataProp(width, d, null, defaultSize) : defaultSize;
207
- const resolvedHeight = height !== undefined ? resolveDataProp(height, d, null, defaultSize) : defaultSize;
204
+ const resolvedWidth =
205
+ width !== undefined ? resolveDataProp(width, d, null, defaultSize) : defaultSize;
206
+ const resolvedHeight =
207
+ height !== undefined ? resolveDataProp(height, d, null, defaultSize) : defaultSize;
208
208
 
209
209
  let resolvedX: number, resolvedY: number;
210
210
  if (geo.projection) {
@@ -247,7 +247,14 @@
247
247
  const key = keyFn(d, i);
248
248
  activeKeys.add(key);
249
249
  const resolved = resolveImage(d);
250
- untrack(() => dataMotionMap.update(key, { x: resolved.x, y: resolved.y, width: resolved.width, height: resolved.height }));
250
+ untrack(() =>
251
+ dataMotionMap.update(key, {
252
+ x: resolved.x,
253
+ y: resolved.y,
254
+ width: resolved.width,
255
+ height: resolved.height,
256
+ })
257
+ );
251
258
  }
252
259
  untrack(() => dataMotionMap.cleanup(activeKeys));
253
260
  });
@@ -416,29 +423,32 @@
416
423
  y: typeof y === 'string' ? y : undefined,
417
424
  };
418
425
  },
419
- canvasRender: layerCtx === 'canvas' ? {
420
- render: canvasRender,
421
- events: {
422
- click: restProps.onclick,
423
- pointerdown: restProps.onpointerdown,
424
- pointerenter: restProps.onpointerenter,
425
- pointermove: restProps.onpointermove,
426
- pointerleave: restProps.onpointerleave,
427
- },
428
- deps: () => [
429
- dataMode,
430
- dataMode ? resolvedItems : null,
431
- motionX.current,
432
- motionY.current,
433
- motionWidth.current,
434
- motionHeight.current,
435
- href,
436
- opacity,
437
- className,
438
- restProps.style,
439
- loadedImageCount,
440
- ],
441
- } : undefined,
426
+ canvasRender:
427
+ layerCtx === 'canvas'
428
+ ? {
429
+ render: canvasRender,
430
+ events: {
431
+ click: restProps.onclick,
432
+ pointerdown: restProps.onpointerdown,
433
+ pointerenter: restProps.onpointerenter,
434
+ pointermove: restProps.onpointermove,
435
+ pointerleave: restProps.onpointerleave,
436
+ },
437
+ deps: () => [
438
+ dataMode,
439
+ dataMode ? resolvedItems : null,
440
+ motionX.current,
441
+ motionY.current,
442
+ motionWidth.current,
443
+ motionHeight.current,
444
+ href,
445
+ opacity,
446
+ className,
447
+ restProps.style,
448
+ loadedImageCount,
449
+ ],
450
+ }
451
+ : undefined,
442
452
  });
443
453
  </script>
444
454
 
@@ -487,7 +497,9 @@
487
497
  width={motionWidth.current}
488
498
  height={motionHeight.current}
489
499
  clip-path={pixelR !== undefined ? `url(#${clipId})` : undefined}
490
- transform={pixelRotate ? `rotate(${pixelRotate}, ${motionX.current}, ${motionY.current})` : undefined}
500
+ transform={pixelRotate
501
+ ? `rotate(${pixelRotate}, ${motionX.current}, ${motionY.current})`
502
+ : undefined}
491
503
  {preserveAspectRatio}
492
504
  crossorigin={crossOrigin}
493
505
  image-rendering={imageRendering}
@@ -173,8 +173,7 @@
173
173
  fill: fillValue,
174
174
  x: point.x + (placement === 'outside' ? -offset : offset),
175
175
  y: point.y,
176
- textAnchor:
177
- placement === 'middle' ? 'middle' : placement === 'outside' ? 'end' : 'start',
176
+ textAnchor: placement === 'middle' ? 'middle' : placement === 'outside' ? 'end' : 'start',
178
177
  verticalAnchor: 'middle',
179
178
  capHeight: '.6rem',
180
179
  };
@@ -185,8 +184,7 @@
185
184
  fill: fillValue,
186
185
  x: point.x + (placement === 'outside' ? offset : -offset),
187
186
  y: point.y,
188
- textAnchor:
189
- placement === 'middle' ? 'middle' : placement === 'outside' ? 'start' : 'end',
187
+ textAnchor: placement === 'middle' ? 'middle' : placement === 'outside' ? 'start' : 'end',
190
188
  verticalAnchor: 'middle',
191
189
  capHeight: '.6rem',
192
190
  };
@@ -298,12 +298,13 @@
298
298
  'lc-line',
299
299
  itemClass ?? (typeof className === 'string' ? className : undefined)
300
300
  ),
301
- style: [
302
- restProps.style as string | undefined,
303
- dashArrayAttr ? `stroke-dasharray: ${dashArrayAttr}` : undefined,
304
- ]
305
- .filter(Boolean)
306
- .join('; ') || undefined,
301
+ style:
302
+ [
303
+ restProps.style as string | undefined,
304
+ dashArrayAttr ? `stroke-dasharray: ${dashArrayAttr}` : undefined,
305
+ ]
306
+ .filter(Boolean)
307
+ .join('; ') || undefined,
307
308
  };
308
309
  }
309
310
 
@@ -174,10 +174,14 @@
174
174
  }
175
175
 
176
176
  if (layerCtx === 'canvas') {
177
- ctx.registerComponent({ name: 'Gradient', kind: 'group', canvasRender: {
178
- render,
179
- deps: () => [x1, y1, x2, y2, stops, className],
180
- } });
177
+ ctx.registerComponent({
178
+ name: 'Gradient',
179
+ kind: 'group',
180
+ canvasRender: {
181
+ render,
182
+ deps: () => [x1, y1, x2, y2, stops, className],
183
+ },
184
+ });
181
185
  }
182
186
  </script>
183
187
 
@@ -171,7 +171,6 @@ TODO:
171
171
  return FALLBACK_COORDS;
172
172
  }
173
173
  });
174
-
175
174
  </script>
176
175
 
177
176
  <Connector
@@ -8,7 +8,15 @@
8
8
  *
9
9
  * Pass `children` to render a custom element/component inside the marker instead.
10
10
  */
11
- type?: 'arrow' | 'triangle' | 'line' | 'circle' | 'circle-stroke' | 'dot' | 'square' | 'square-stroke';
11
+ type?:
12
+ | 'arrow'
13
+ | 'triangle'
14
+ | 'line'
15
+ | 'circle'
16
+ | 'circle-stroke'
17
+ | 'dot'
18
+ | 'square'
19
+ | 'square-stroke';
12
20
 
13
21
  /**
14
22
  * Unique identifier for the marker
@@ -197,30 +197,50 @@
197
197
  const strokeKey = layerCtx === 'canvas' ? createKey(() => stroke) : undefined;
198
198
 
199
199
  if (layerCtx === 'canvas') {
200
- ctx.registerComponent({ name: 'Path', kind: 'mark', canvasRender: {
201
- render,
202
- events: {
203
- get click() { return onclick; },
204
- get pointerenter() { return onpointerenter; },
205
- get pointermove() { return onpointermove; },
206
- get pointerleave() { return onpointerleave; },
207
- get pointerdown() { return onpointerdown; },
208
- get pointerover() { return onpointerover; },
209
- get pointerout() { return onpointerout; },
210
- get touchmove() { return ontouchmove; },
200
+ ctx.registerComponent({
201
+ name: 'Path',
202
+ kind: 'mark',
203
+ canvasRender: {
204
+ render,
205
+ events: {
206
+ get click() {
207
+ return onclick;
208
+ },
209
+ get pointerenter() {
210
+ return onpointerenter;
211
+ },
212
+ get pointermove() {
213
+ return onpointermove;
214
+ },
215
+ get pointerleave() {
216
+ return onpointerleave;
217
+ },
218
+ get pointerdown() {
219
+ return onpointerdown;
220
+ },
221
+ get pointerover() {
222
+ return onpointerover;
223
+ },
224
+ get pointerout() {
225
+ return onpointerout;
226
+ },
227
+ get touchmove() {
228
+ return ontouchmove;
229
+ },
230
+ },
231
+ deps: () => [
232
+ fillKey!.current,
233
+ fillOpacity,
234
+ strokeKey!.current,
235
+ strokeOpacity,
236
+ strokeWidth,
237
+ opacity,
238
+ className,
239
+ tweenedState.current,
240
+ restProps.style,
241
+ ],
211
242
  },
212
- deps: () => [
213
- fillKey!.current,
214
- fillOpacity,
215
- strokeKey!.current,
216
- strokeOpacity,
217
- strokeWidth,
218
- opacity,
219
- className,
220
- tweenedState.current,
221
- restProps.style,
222
- ],
223
- } });
243
+ });
224
244
  }
225
245
 
226
246
  let startPoint = $state<DOMPoint | undefined>();
@@ -95,7 +95,9 @@
95
95
  background?: string;
96
96
 
97
97
  /**
98
- * Render as a child of the pattern
98
+ * Render as a child of the pattern.
99
+ *
100
+ * Note: only supported on the `<Svg>` layer.
99
101
  */
100
102
  patternContent?: Snippet;
101
103
 
@@ -250,10 +252,102 @@
250
252
  }
251
253
 
252
254
  if (layerCtx === 'canvas') {
253
- chartCtx.registerComponent({ name: 'Pattern', kind: 'mark', canvasRender: {
254
- render,
255
- deps: () => [width, height, shapes, background],
256
- } });
255
+ chartCtx.registerComponent({
256
+ name: 'Pattern',
257
+ kind: 'group',
258
+ canvasRender: {
259
+ render,
260
+ deps: () => [width, height, shapes, background],
261
+ },
262
+ });
263
+ }
264
+
265
+ function withOpacity(color: string, opacity: number) {
266
+ return opacity === 1 ? color : `color-mix(in srgb, ${color} ${opacity * 100}%, transparent)`;
267
+ }
268
+
269
+ // Build a CSS `background` value from lines/circles/background.
270
+ // Uses repeating-linear-gradient for lines and radial-gradient for circles
271
+ // so CSS variables/classes resolve natively in the HTML layer.
272
+ function createCSSPattern(): string {
273
+ const layers: string[] = [];
274
+
275
+ if (linesProp) {
276
+ const lineDefs = Array.isArray(linesProp)
277
+ ? linesProp
278
+ : linesProp === true
279
+ ? [{}]
280
+ : [linesProp];
281
+ for (const line of lineDefs) {
282
+ const color = withOpacity(
283
+ line.color ?? 'var(--color-surface-content, currentColor)',
284
+ line.opacity ?? 1
285
+ );
286
+ const sw = line.width ?? 1;
287
+
288
+ let rotate = Math.round(line.rotate ?? 0) % 360;
289
+ if (rotate > 180) rotate = rotate - 360;
290
+ else if (rotate > 90) rotate = rotate - 180;
291
+ else if (rotate < -180) rotate = rotate + 360;
292
+ else if (rotate < -90) rotate = rotate + 180;
293
+
294
+ let angle: number;
295
+ let period: number;
296
+ if (rotate === 0) {
297
+ angle = 0;
298
+ period = height;
299
+ } else if (rotate === 90) {
300
+ angle = 90;
301
+ period = width;
302
+ } else if (rotate > 0) {
303
+ angle = 45;
304
+ period = (width * height) / Math.sqrt(width * width + height * height);
305
+ } else {
306
+ angle = 135;
307
+ period = (width * height) / Math.sqrt(width * width + height * height);
308
+ }
309
+
310
+ layers.push(
311
+ `repeating-linear-gradient(${angle}deg, ${color} 0 ${sw}px, transparent ${sw}px ${period}px)`
312
+ );
313
+ }
314
+ }
315
+
316
+ if (circlesProp) {
317
+ const circleDefs = Array.isArray(circlesProp)
318
+ ? circlesProp
319
+ : circlesProp === true
320
+ ? [{}]
321
+ : [circlesProp];
322
+ for (const circle of circleDefs) {
323
+ const color = withOpacity(
324
+ circle.color ?? 'var(--color-surface-content, currentColor)',
325
+ circle.opacity ?? 1
326
+ );
327
+ const r = circle.radius ?? 1;
328
+
329
+ if (circle.stagger) {
330
+ layers.push(
331
+ `radial-gradient(circle at 25% 25%, ${color} ${r}px, transparent ${r}px) 0 0 / ${size}px ${size}px`,
332
+ `radial-gradient(circle at 75% 75%, ${color} ${r}px, transparent ${r}px) 0 0 / ${size}px ${size}px`
333
+ );
334
+ } else {
335
+ layers.push(
336
+ `radial-gradient(circle at center, ${color} ${r}px, transparent ${r}px) 0 0 / ${size}px ${size}px`
337
+ );
338
+ }
339
+ }
340
+ }
341
+
342
+ // `background` may be either a plain color or an image value (e.g. `linear-gradient(...)`
343
+ // when used as `<Pattern background={gradient}>`). Images go in the image-layer list
344
+ // tiled at the pattern size (after shape layers so they paint underneath);
345
+ // colors go in the background-color slot.
346
+ const isImage = background != null && /gradient\(|url\(/i.test(background);
347
+ if (isImage) layers.push(`${background} 0 0 / ${width}px ${height}px`);
348
+
349
+ if (layers.length === 0) return background ?? 'transparent';
350
+ return !isImage && background ? `${layers.join(', ')}, ${background}` : layers.join(', ');
257
351
  }
258
352
  </script>
259
353
 
@@ -299,4 +393,6 @@
299
393
  </defs>
300
394
 
301
395
  {@render children?.({ id, pattern: `url(#${id})` })}
396
+ {:else if layerCtx === 'html'}
397
+ {@render children?.({ id, pattern: createCSSPattern() })}
302
398
  {/if}
@@ -75,7 +75,9 @@ export type PatternPropsWithoutHTML = {
75
75
  */
76
76
  background?: string;
77
77
  /**
78
- * Render as a child of the pattern
78
+ * Render as a child of the pattern.
79
+ *
80
+ * Note: only supported on the `<Svg>` layer.
79
81
  */
80
82
  patternContent?: Snippet;
81
83
  children?: Snippet<[{
@@ -109,9 +109,7 @@
109
109
 
110
110
  const endAngle = $derived(
111
111
  endAngleProp ??
112
- degreesToRadians(
113
- (ctx.config.xRange ? max(ctx.config.xRange as number[]) : max(range))!
114
- )
112
+ degreesToRadians((ctx.config.xRange ? max(ctx.config.xRange as number[]) : max(range))!)
115
113
  );
116
114
 
117
115
  const motionEndAngle = createMotion(0, () => endAngle, motion);
@@ -120,9 +118,7 @@
120
118
  let _pie = d3pie<any>()
121
119
  .startAngle(
122
120
  startAngleProp ??
123
- degreesToRadians(
124
- (ctx.config.xRange ? min(ctx.config.xRange as number[]) : min(range))!
125
- )
121
+ degreesToRadians((ctx.config.xRange ? min(ctx.config.xRange as number[]) : min(range))!)
126
122
  )
127
123
  .endAngle(motionEndAngle.current)
128
124
  .padAngle(padAngle)
@@ -144,10 +144,14 @@
144
144
  }
145
145
 
146
146
  if (layerCtx === 'canvas') {
147
- ctx.registerComponent({ name: 'Gradient', kind: 'group', canvasRender: {
148
- render,
149
- deps: () => [stops, cx, cy, fx, fy, ctx.width, ctx.height],
150
- } });
147
+ ctx.registerComponent({
148
+ name: 'Gradient',
149
+ kind: 'group',
150
+ canvasRender: {
151
+ render,
152
+ deps: () => [stops, cx, cy, fx, fy, ctx.width, ctx.height],
153
+ },
154
+ });
151
155
  }
152
156
  </script>
153
157
 
@@ -313,9 +313,7 @@
313
313
  const resolved = resolveCorners(corners, Infinity, Infinity);
314
314
  return cornersUniform(resolved) ? resolved[0] : undefined;
315
315
  });
316
- const cornersNonUniform = $derived(
317
- corners !== undefined && cornersUniformValue === undefined
318
- );
316
+ const cornersNonUniform = $derived(corners !== undefined && cornersUniformValue === undefined);
319
317
 
320
318
  // Normalize rx/ry - if only one is provided, use it for both (SVG behavior)
321
319
  // Coerce to number for canvas rendering (SVG allows string like "50%")
@@ -368,8 +366,14 @@
368
366
  const staticStrokeWidth = $derived(typeof strokeWidth === 'number' ? strokeWidth : undefined);
369
367
  const staticOpacity = $derived(typeof opacity === 'number' ? opacity : undefined);
370
368
  const staticClassName = $derived(typeof className === 'string' ? className : undefined);
369
+ // Match SVG's implicit `stroke-width: 1` default: if `stroke` is set but
370
+ // `strokeWidth` is not, render a 1px border so HTML matches SVG/Canvas layers.
371
371
  const staticBorderWidth = $derived(
372
- typeof strokeWidth === 'number' ? `${strokeWidth}px` : undefined
372
+ typeof strokeWidth === 'number'
373
+ ? `${strokeWidth}px`
374
+ : typeof stroke === 'string'
375
+ ? '1px'
376
+ : undefined
373
377
  );
374
378
  const htmlRestProps = $derived(restProps as unknown as HTMLAttributes<HTMLDivElement>);
375
379
 
@@ -430,12 +434,13 @@
430
434
  'lc-rect',
431
435
  itemClass ?? (typeof className === 'string' ? className : undefined)
432
436
  ),
433
- style: [
434
- restProps.style as string | undefined,
435
- dashArrayAttr ? `stroke-dasharray: ${dashArrayAttr}` : undefined,
436
- ]
437
- .filter(Boolean)
438
- .join('; ') || undefined,
437
+ style:
438
+ [
439
+ restProps.style as string | undefined,
440
+ dashArrayAttr ? `stroke-dasharray: ${dashArrayAttr}` : undefined,
441
+ ]
442
+ .filter(Boolean)
443
+ .join('; ') || undefined,
439
444
  };
440
445
  }
441
446
 
@@ -596,7 +601,7 @@
596
601
  opacity={staticOpacity}
597
602
  stroke-dasharray={dashArrayAttr}
598
603
  class={cls('lc-rect', staticClassName)}
599
- {...(restProps as unknown as SVGAttributes<SVGPathElement>)}
604
+ {...restProps as unknown as SVGAttributes<SVGPathElement>}
600
605
  {onclick}
601
606
  {ondblclick}
602
607
  {onpointerenter}
@@ -641,6 +646,12 @@
641
646
  {@const resolvedStrokeWidth = resolveStyleProp(strokeWidth, item.d)}
642
647
  {@const resolvedOpacity = resolveStyleProp(opacity, item.d)}
643
648
  {@const resolvedClass = resolveStyleProp(className, item.d)}
649
+ {@const resolvedBorderWidth =
650
+ resolvedStrokeWidth != null
651
+ ? `${resolvedStrokeWidth}px`
652
+ : resolvedStroke != null
653
+ ? '1px'
654
+ : undefined}
644
655
  <!-- svelte-ignore a11y_click_events_have_key_events -->
645
656
  <!-- svelte-ignore a11y_no_static_element_interactions -->
646
657
  <div
@@ -650,8 +661,9 @@
650
661
  style:width="{item.width}px"
651
662
  style:height="{item.height}px"
652
663
  style:background={resolvedFill}
664
+ style:background-origin="border-box"
653
665
  style:opacity={resolvedOpacity}
654
- style:border-width="{resolvedStrokeWidth}px"
666
+ style:border-width={resolvedBorderWidth}
655
667
  style:border-style={dashArrayResolved ? 'dashed' : 'solid'}
656
668
  style:border-color={resolvedStroke}
657
669
  style:border-radius="{rx}px"
@@ -676,6 +688,7 @@
676
688
  style:width="{motionWidth.current}px"
677
689
  style:height="{motionHeight.current}px"
678
690
  style:background={staticFill}
691
+ style:background-origin="border-box"
679
692
  style:opacity={staticOpacity}
680
693
  style:border-width={staticBorderWidth}
681
694
  style:border-style={dashArrayResolved ? 'dashed' : 'solid'}
@@ -712,6 +725,10 @@
712
725
  }
713
726
 
714
727
  /* Html layers */
728
+ :global(:where(.lc-layout-html .lc-rect)) {
729
+ /* Match SVG sizing/positioning (visual extent equals `width`×`height`, border on outer edge) */
730
+ box-sizing: border-box;
731
+ }
715
732
  :global(:where(.lc-layout-html .lc-rect):not([background])) {
716
733
  background: var(--fill-color);
717
734
  }