@camstack/ui-library 1.2.178 → 1.2.179

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 (3) hide show
  1. package/dist/index.cjs +114 -19
  2. package/dist/index.js +114 -19
  3. package/package.json +1 -1
package/dist/index.cjs CHANGED
@@ -16806,6 +16806,25 @@ var FULL_FRAME = {
16806
16806
  var MAX_CELLS = 16;
16807
16807
  /** Hoisted so `useDevices` does not re-subscribe on every render. */
16808
16808
  var CAMERA_FILTER = { type: _camstack_types.DeviceType.Camera };
16809
+ /**
16810
+ * A rectangle being dragged must not also PAN THE PAGE.
16811
+ *
16812
+ * On a phone a pointerdown on a `div` is a scroll gesture until the browser is
16813
+ * told otherwise, and `preventDefault` in the handler is too late — the browser
16814
+ * has already decided. `touch-action: none` is the only thing that makes a drag
16815
+ * work with a finger, and every surface that starts one carries it.
16816
+ */
16817
+ var DRAG_SURFACE = { touchAction: "none" };
16818
+ /**
16819
+ * Finger-sized hit area around a grip.
16820
+ *
16821
+ * The visible square stays small (it sits ON the picture being edited and a big
16822
+ * one hides what it is positioning); the TOUCHABLE box around it is 24px, which
16823
+ * is the smallest target size that does not need a fingernail. The two are
16824
+ * different boxes on purpose — making the dot itself 24px would cover a quarter
16825
+ * of a small cell.
16826
+ */
16827
+ var HANDLE_HIT = "absolute flex h-6 w-6 items-center justify-center";
16809
16828
  /** The eight grips, in the order they are drawn. */
16810
16829
  var HANDLES = [
16811
16830
  "nw",
@@ -16818,15 +16837,26 @@ var HANDLES = [
16818
16837
  "w"
16819
16838
  ];
16820
16839
  var HANDLE_STYLE = {
16821
- nw: "-top-1 -left-1 cursor-nw-resize",
16822
- n: "-top-1 left-1/2 -translate-x-1/2 cursor-n-resize",
16823
- ne: "-top-1 -right-1 cursor-ne-resize",
16824
- e: "top-1/2 -right-1 -translate-y-1/2 cursor-e-resize",
16825
- se: "-bottom-1 -right-1 cursor-se-resize",
16826
- s: "-bottom-1 left-1/2 -translate-x-1/2 cursor-s-resize",
16827
- sw: "-bottom-1 -left-1 cursor-sw-resize",
16828
- w: "top-1/2 -left-1 -translate-y-1/2 cursor-w-resize"
16840
+ nw: "-top-3 -left-3 cursor-nw-resize",
16841
+ n: "-top-3 left-1/2 -translate-x-1/2 cursor-n-resize",
16842
+ ne: "-top-3 -right-3 cursor-ne-resize",
16843
+ e: "top-1/2 -right-3 -translate-y-1/2 cursor-e-resize",
16844
+ se: "-bottom-3 -right-3 cursor-se-resize",
16845
+ s: "-bottom-3 left-1/2 -translate-x-1/2 cursor-s-resize",
16846
+ sw: "-bottom-3 -left-3 cursor-sw-resize",
16847
+ w: "top-1/2 -left-3 -translate-y-1/2 cursor-w-resize"
16829
16848
  };
16849
+ /**
16850
+ * Start a drag and KEEP it, even when the finger leaves the rectangle.
16851
+ *
16852
+ * Without pointer capture a drag ends the moment the pointer crosses the box it
16853
+ * started in — which on a phone is immediately, because the box is small and
16854
+ * the finger is not. With capture the events keep coming to the element that
16855
+ * was grabbed and still bubble to the canvas that does the arithmetic.
16856
+ */
16857
+ function capturePointer(event) {
16858
+ event.currentTarget.setPointerCapture(event.pointerId);
16859
+ }
16830
16860
  /** Move or resize, from one drag and one delta. */
16831
16861
  function applyDrag(drag, dx, dy) {
16832
16862
  return drag.mode === "move" ? moveNormalizedRect(drag.origin, dx, dy) : resizeNormalizedRectFrom(drag.origin, drag.mode, dx, dy);
@@ -16966,12 +16996,15 @@ function CameraGridLayoutEditor({ deviceId }) {
16966
16996
  }),
16967
16997
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
16968
16998
  className: "relative w-full select-none overflow-hidden rounded border border-border bg-black",
16969
- style: { aspectRatio: String(canvasAspect) },
16999
+ style: {
17000
+ aspectRatio: String(canvasAspect),
17001
+ ...DRAG_SURFACE
17002
+ },
16970
17003
  onPointerMove,
16971
17004
  onPointerUp: () => {
16972
17005
  setDrag(null);
16973
17006
  },
16974
- onPointerLeave: () => {
17007
+ onPointerCancel: () => {
16975
17008
  setDrag(null);
16976
17009
  },
16977
17010
  children: [draft.cells.map((cell, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GridCellBox, {
@@ -17117,10 +17150,12 @@ function GridCellBox({ cell, selected, onSelect, onDragStart }) {
17117
17150
  role: "presentation",
17118
17151
  onPointerDown: (event) => {
17119
17152
  onSelect();
17153
+ capturePointer(event);
17120
17154
  onDragStart("move", event);
17121
17155
  },
17122
17156
  className: `absolute cursor-move border ${selected ? "border-accent" : "border-border/60"}`,
17123
17157
  style: {
17158
+ ...DRAG_SURFACE,
17124
17159
  left: `${String(cell.cell.x * 100)}%`,
17125
17160
  top: `${String(cell.cell.y * 100)}%`,
17126
17161
  width: `${String(cell.cell.width * 100)}%`,
@@ -17134,9 +17169,12 @@ function GridCellBox({ cell, selected, onSelect, onDragStart }) {
17134
17169
  role: "presentation",
17135
17170
  onPointerDown: (event) => {
17136
17171
  event.stopPropagation();
17172
+ capturePointer(event);
17137
17173
  onDragStart(handle, event);
17138
17174
  },
17139
- className: `absolute h-2.5 w-2.5 bg-accent ${HANDLE_STYLE[handle]}`
17175
+ className: `${HANDLE_HIT} ${HANDLE_STYLE[handle]}`,
17176
+ style: DRAG_SURFACE,
17177
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "h-2.5 w-2.5 bg-accent" })
17140
17178
  }, handle))
17141
17179
  });
17142
17180
  }
@@ -17181,16 +17219,15 @@ function CellInspector({ cell, index, cameras, onChange, onRemove }) {
17181
17219
  }
17182
17220
  }),
17183
17221
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
17184
- className: "grid grid-cols-4 gap-2",
17222
+ className: "grid grid-cols-2 gap-x-3 gap-y-1.5",
17185
17223
  children: [
17186
17224
  "x",
17187
17225
  "y",
17188
17226
  "width",
17189
17227
  "height"
17190
- ].map((key) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LabelledNumber, {
17228
+ ].map((key) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CropSlider, {
17191
17229
  label: `Crop ${key}`,
17192
- value: Math.round(cell.source[key] * 100) / 100,
17193
- step: .01,
17230
+ value: cell.source[key],
17194
17231
  onChange: (next) => {
17195
17232
  onChange({ source: clampNormalizedRect({
17196
17233
  ...cell.source,
@@ -17218,6 +17255,7 @@ function CellInspector({ cell, index, cameras, onChange, onRemove }) {
17218
17255
  function SourceCropEditor({ deviceId, source, onChange }) {
17219
17256
  const { src } = useDeviceSnapshotImage(deviceId, { width: 640 });
17220
17257
  const [drag, setDrag] = (0, react$1.useState)(null);
17258
+ const [sourceAspect, setSourceAspect] = (0, react$1.useState)(null);
17221
17259
  const onPointerMove = (event) => {
17222
17260
  if (drag === null) return;
17223
17261
  const bounds = event.currentTarget.getBoundingClientRect();
@@ -17247,12 +17285,15 @@ function SourceCropEditor({ deviceId, source, onChange }) {
17247
17285
  })]
17248
17286
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
17249
17287
  className: "relative w-full select-none overflow-hidden rounded border border-border bg-black",
17250
- style: { aspectRatio: "16 / 9" },
17288
+ style: {
17289
+ aspectRatio: String(sourceAspect ?? 16 / 9),
17290
+ ...DRAG_SURFACE
17291
+ },
17251
17292
  onPointerMove,
17252
17293
  onPointerUp: () => {
17253
17294
  setDrag(null);
17254
17295
  },
17255
- onPointerLeave: () => {
17296
+ onPointerCancel: () => {
17256
17297
  setDrag(null);
17257
17298
  },
17258
17299
  children: [
@@ -17262,12 +17303,17 @@ function SourceCropEditor({ deviceId, source, onChange }) {
17262
17303
  }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
17263
17304
  src,
17264
17305
  alt: "",
17265
- className: "absolute inset-0 h-full w-full object-cover"
17306
+ className: "absolute inset-0 h-full w-full",
17307
+ onLoad: (event) => {
17308
+ const { naturalWidth, naturalHeight } = event.currentTarget;
17309
+ if (naturalWidth > 0 && naturalHeight > 0) setSourceAspect(naturalWidth / naturalHeight);
17310
+ }
17266
17311
  }),
17267
17312
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: "pointer-events-none absolute inset-0 bg-black/55" }),
17268
17313
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
17269
17314
  role: "presentation",
17270
17315
  onPointerDown: (event) => {
17316
+ capturePointer(event);
17271
17317
  setDrag({
17272
17318
  mode: "move",
17273
17319
  startX: event.clientX,
@@ -17277,6 +17323,7 @@ function SourceCropEditor({ deviceId, source, onChange }) {
17277
17323
  },
17278
17324
  className: "absolute cursor-move border border-accent",
17279
17325
  style: {
17326
+ ...DRAG_SURFACE,
17280
17327
  left: `${String(source.x * 100)}%`,
17281
17328
  top: `${String(source.y * 100)}%`,
17282
17329
  width: `${String(source.width * 100)}%`,
@@ -17289,6 +17336,7 @@ function SourceCropEditor({ deviceId, source, onChange }) {
17289
17336
  role: "presentation",
17290
17337
  onPointerDown: (event) => {
17291
17338
  event.stopPropagation();
17339
+ capturePointer(event);
17292
17340
  setDrag({
17293
17341
  mode: handle,
17294
17342
  startX: event.clientX,
@@ -17296,13 +17344,60 @@ function SourceCropEditor({ deviceId, source, onChange }) {
17296
17344
  origin: source
17297
17345
  });
17298
17346
  },
17299
- className: `absolute h-2.5 w-2.5 bg-accent ${HANDLE_STYLE[handle]}`
17347
+ className: `${HANDLE_HIT} ${HANDLE_STYLE[handle]}`,
17348
+ style: DRAG_SURFACE,
17349
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "h-2.5 w-2.5 bg-accent" })
17300
17350
  }, handle))
17301
17351
  })
17302
17352
  ]
17303
17353
  })]
17304
17354
  });
17305
17355
  }
17356
+ /**
17357
+ * One crop number, as a slider AND as a number.
17358
+ *
17359
+ * The slider is what makes this usable with a thumb — a `number` input on a
17360
+ * phone opens a keyboard, and adjusting a crop by typing 0.37 then 0.39 then
17361
+ * 0.38 is not adjusting, it is guessing. The typed field stays because a slider
17362
+ * cannot express "exactly a third" and because a value you can read is how you
17363
+ * copy a crop from one cell to another.
17364
+ *
17365
+ * Both write through `clampNormalizedRect` upstream, so neither can hand the
17366
+ * addon a rectangle that leaves the frame — it THROWS on one, by design.
17367
+ */
17368
+ function CropSlider({ label, value, onChange }) {
17369
+ const rounded = Math.round(value * 100) / 100;
17370
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
17371
+ className: "flex flex-col gap-0.5",
17372
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
17373
+ className: "flex items-baseline justify-between text-foreground-subtle",
17374
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: label }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
17375
+ type: "number",
17376
+ min: 0,
17377
+ max: 1,
17378
+ step: .01,
17379
+ value: rounded,
17380
+ onChange: (event) => {
17381
+ const next = Number(event.target.value);
17382
+ if (Number.isFinite(next)) onChange(next);
17383
+ },
17384
+ className: "w-12 rounded border border-border bg-transparent px-1 py-0.5 text-right"
17385
+ })]
17386
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
17387
+ type: "range",
17388
+ min: 0,
17389
+ max: 1,
17390
+ step: .005,
17391
+ value,
17392
+ onChange: (event) => {
17393
+ const next = Number(event.target.value);
17394
+ if (Number.isFinite(next)) onChange(next);
17395
+ },
17396
+ style: DRAG_SURFACE,
17397
+ className: "h-5 w-full accent-accent"
17398
+ })]
17399
+ });
17400
+ }
17306
17401
  function LabelledNumber({ label, value, step = 1, onChange }) {
17307
17402
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
17308
17403
  className: "flex flex-col gap-0.5",
package/dist/index.js CHANGED
@@ -16782,6 +16782,25 @@ var FULL_FRAME = {
16782
16782
  var MAX_CELLS = 16;
16783
16783
  /** Hoisted so `useDevices` does not re-subscribe on every render. */
16784
16784
  var CAMERA_FILTER = { type: DeviceType.Camera };
16785
+ /**
16786
+ * A rectangle being dragged must not also PAN THE PAGE.
16787
+ *
16788
+ * On a phone a pointerdown on a `div` is a scroll gesture until the browser is
16789
+ * told otherwise, and `preventDefault` in the handler is too late — the browser
16790
+ * has already decided. `touch-action: none` is the only thing that makes a drag
16791
+ * work with a finger, and every surface that starts one carries it.
16792
+ */
16793
+ var DRAG_SURFACE = { touchAction: "none" };
16794
+ /**
16795
+ * Finger-sized hit area around a grip.
16796
+ *
16797
+ * The visible square stays small (it sits ON the picture being edited and a big
16798
+ * one hides what it is positioning); the TOUCHABLE box around it is 24px, which
16799
+ * is the smallest target size that does not need a fingernail. The two are
16800
+ * different boxes on purpose — making the dot itself 24px would cover a quarter
16801
+ * of a small cell.
16802
+ */
16803
+ var HANDLE_HIT = "absolute flex h-6 w-6 items-center justify-center";
16785
16804
  /** The eight grips, in the order they are drawn. */
16786
16805
  var HANDLES = [
16787
16806
  "nw",
@@ -16794,15 +16813,26 @@ var HANDLES = [
16794
16813
  "w"
16795
16814
  ];
16796
16815
  var HANDLE_STYLE = {
16797
- nw: "-top-1 -left-1 cursor-nw-resize",
16798
- n: "-top-1 left-1/2 -translate-x-1/2 cursor-n-resize",
16799
- ne: "-top-1 -right-1 cursor-ne-resize",
16800
- e: "top-1/2 -right-1 -translate-y-1/2 cursor-e-resize",
16801
- se: "-bottom-1 -right-1 cursor-se-resize",
16802
- s: "-bottom-1 left-1/2 -translate-x-1/2 cursor-s-resize",
16803
- sw: "-bottom-1 -left-1 cursor-sw-resize",
16804
- w: "top-1/2 -left-1 -translate-y-1/2 cursor-w-resize"
16816
+ nw: "-top-3 -left-3 cursor-nw-resize",
16817
+ n: "-top-3 left-1/2 -translate-x-1/2 cursor-n-resize",
16818
+ ne: "-top-3 -right-3 cursor-ne-resize",
16819
+ e: "top-1/2 -right-3 -translate-y-1/2 cursor-e-resize",
16820
+ se: "-bottom-3 -right-3 cursor-se-resize",
16821
+ s: "-bottom-3 left-1/2 -translate-x-1/2 cursor-s-resize",
16822
+ sw: "-bottom-3 -left-3 cursor-sw-resize",
16823
+ w: "top-1/2 -left-3 -translate-y-1/2 cursor-w-resize"
16805
16824
  };
16825
+ /**
16826
+ * Start a drag and KEEP it, even when the finger leaves the rectangle.
16827
+ *
16828
+ * Without pointer capture a drag ends the moment the pointer crosses the box it
16829
+ * started in — which on a phone is immediately, because the box is small and
16830
+ * the finger is not. With capture the events keep coming to the element that
16831
+ * was grabbed and still bubble to the canvas that does the arithmetic.
16832
+ */
16833
+ function capturePointer(event) {
16834
+ event.currentTarget.setPointerCapture(event.pointerId);
16835
+ }
16806
16836
  /** Move or resize, from one drag and one delta. */
16807
16837
  function applyDrag(drag, dx, dy) {
16808
16838
  return drag.mode === "move" ? moveNormalizedRect(drag.origin, dx, dy) : resizeNormalizedRectFrom(drag.origin, drag.mode, dx, dy);
@@ -16942,12 +16972,15 @@ function CameraGridLayoutEditor({ deviceId }) {
16942
16972
  }),
16943
16973
  /* @__PURE__ */ jsxs("div", {
16944
16974
  className: "relative w-full select-none overflow-hidden rounded border border-border bg-black",
16945
- style: { aspectRatio: String(canvasAspect) },
16975
+ style: {
16976
+ aspectRatio: String(canvasAspect),
16977
+ ...DRAG_SURFACE
16978
+ },
16946
16979
  onPointerMove,
16947
16980
  onPointerUp: () => {
16948
16981
  setDrag(null);
16949
16982
  },
16950
- onPointerLeave: () => {
16983
+ onPointerCancel: () => {
16951
16984
  setDrag(null);
16952
16985
  },
16953
16986
  children: [draft.cells.map((cell, index) => /* @__PURE__ */ jsx(GridCellBox, {
@@ -17093,10 +17126,12 @@ function GridCellBox({ cell, selected, onSelect, onDragStart }) {
17093
17126
  role: "presentation",
17094
17127
  onPointerDown: (event) => {
17095
17128
  onSelect();
17129
+ capturePointer(event);
17096
17130
  onDragStart("move", event);
17097
17131
  },
17098
17132
  className: `absolute cursor-move border ${selected ? "border-accent" : "border-border/60"}`,
17099
17133
  style: {
17134
+ ...DRAG_SURFACE,
17100
17135
  left: `${String(cell.cell.x * 100)}%`,
17101
17136
  top: `${String(cell.cell.y * 100)}%`,
17102
17137
  width: `${String(cell.cell.width * 100)}%`,
@@ -17110,9 +17145,12 @@ function GridCellBox({ cell, selected, onSelect, onDragStart }) {
17110
17145
  role: "presentation",
17111
17146
  onPointerDown: (event) => {
17112
17147
  event.stopPropagation();
17148
+ capturePointer(event);
17113
17149
  onDragStart(handle, event);
17114
17150
  },
17115
- className: `absolute h-2.5 w-2.5 bg-accent ${HANDLE_STYLE[handle]}`
17151
+ className: `${HANDLE_HIT} ${HANDLE_STYLE[handle]}`,
17152
+ style: DRAG_SURFACE,
17153
+ children: /* @__PURE__ */ jsx("span", { className: "h-2.5 w-2.5 bg-accent" })
17116
17154
  }, handle))
17117
17155
  });
17118
17156
  }
@@ -17157,16 +17195,15 @@ function CellInspector({ cell, index, cameras, onChange, onRemove }) {
17157
17195
  }
17158
17196
  }),
17159
17197
  /* @__PURE__ */ jsx("div", {
17160
- className: "grid grid-cols-4 gap-2",
17198
+ className: "grid grid-cols-2 gap-x-3 gap-y-1.5",
17161
17199
  children: [
17162
17200
  "x",
17163
17201
  "y",
17164
17202
  "width",
17165
17203
  "height"
17166
- ].map((key) => /* @__PURE__ */ jsx(LabelledNumber, {
17204
+ ].map((key) => /* @__PURE__ */ jsx(CropSlider, {
17167
17205
  label: `Crop ${key}`,
17168
- value: Math.round(cell.source[key] * 100) / 100,
17169
- step: .01,
17206
+ value: cell.source[key],
17170
17207
  onChange: (next) => {
17171
17208
  onChange({ source: clampNormalizedRect({
17172
17209
  ...cell.source,
@@ -17194,6 +17231,7 @@ function CellInspector({ cell, index, cameras, onChange, onRemove }) {
17194
17231
  function SourceCropEditor({ deviceId, source, onChange }) {
17195
17232
  const { src } = useDeviceSnapshotImage(deviceId, { width: 640 });
17196
17233
  const [drag, setDrag] = useState(null);
17234
+ const [sourceAspect, setSourceAspect] = useState(null);
17197
17235
  const onPointerMove = (event) => {
17198
17236
  if (drag === null) return;
17199
17237
  const bounds = event.currentTarget.getBoundingClientRect();
@@ -17223,12 +17261,15 @@ function SourceCropEditor({ deviceId, source, onChange }) {
17223
17261
  })]
17224
17262
  }), /* @__PURE__ */ jsxs("div", {
17225
17263
  className: "relative w-full select-none overflow-hidden rounded border border-border bg-black",
17226
- style: { aspectRatio: "16 / 9" },
17264
+ style: {
17265
+ aspectRatio: String(sourceAspect ?? 16 / 9),
17266
+ ...DRAG_SURFACE
17267
+ },
17227
17268
  onPointerMove,
17228
17269
  onPointerUp: () => {
17229
17270
  setDrag(null);
17230
17271
  },
17231
- onPointerLeave: () => {
17272
+ onPointerCancel: () => {
17232
17273
  setDrag(null);
17233
17274
  },
17234
17275
  children: [
@@ -17238,12 +17279,17 @@ function SourceCropEditor({ deviceId, source, onChange }) {
17238
17279
  }) : /* @__PURE__ */ jsx("img", {
17239
17280
  src,
17240
17281
  alt: "",
17241
- className: "absolute inset-0 h-full w-full object-cover"
17282
+ className: "absolute inset-0 h-full w-full",
17283
+ onLoad: (event) => {
17284
+ const { naturalWidth, naturalHeight } = event.currentTarget;
17285
+ if (naturalWidth > 0 && naturalHeight > 0) setSourceAspect(naturalWidth / naturalHeight);
17286
+ }
17242
17287
  }),
17243
17288
  /* @__PURE__ */ jsx("div", { className: "pointer-events-none absolute inset-0 bg-black/55" }),
17244
17289
  /* @__PURE__ */ jsx("div", {
17245
17290
  role: "presentation",
17246
17291
  onPointerDown: (event) => {
17292
+ capturePointer(event);
17247
17293
  setDrag({
17248
17294
  mode: "move",
17249
17295
  startX: event.clientX,
@@ -17253,6 +17299,7 @@ function SourceCropEditor({ deviceId, source, onChange }) {
17253
17299
  },
17254
17300
  className: "absolute cursor-move border border-accent",
17255
17301
  style: {
17302
+ ...DRAG_SURFACE,
17256
17303
  left: `${String(source.x * 100)}%`,
17257
17304
  top: `${String(source.y * 100)}%`,
17258
17305
  width: `${String(source.width * 100)}%`,
@@ -17265,6 +17312,7 @@ function SourceCropEditor({ deviceId, source, onChange }) {
17265
17312
  role: "presentation",
17266
17313
  onPointerDown: (event) => {
17267
17314
  event.stopPropagation();
17315
+ capturePointer(event);
17268
17316
  setDrag({
17269
17317
  mode: handle,
17270
17318
  startX: event.clientX,
@@ -17272,13 +17320,60 @@ function SourceCropEditor({ deviceId, source, onChange }) {
17272
17320
  origin: source
17273
17321
  });
17274
17322
  },
17275
- className: `absolute h-2.5 w-2.5 bg-accent ${HANDLE_STYLE[handle]}`
17323
+ className: `${HANDLE_HIT} ${HANDLE_STYLE[handle]}`,
17324
+ style: DRAG_SURFACE,
17325
+ children: /* @__PURE__ */ jsx("span", { className: "h-2.5 w-2.5 bg-accent" })
17276
17326
  }, handle))
17277
17327
  })
17278
17328
  ]
17279
17329
  })]
17280
17330
  });
17281
17331
  }
17332
+ /**
17333
+ * One crop number, as a slider AND as a number.
17334
+ *
17335
+ * The slider is what makes this usable with a thumb — a `number` input on a
17336
+ * phone opens a keyboard, and adjusting a crop by typing 0.37 then 0.39 then
17337
+ * 0.38 is not adjusting, it is guessing. The typed field stays because a slider
17338
+ * cannot express "exactly a third" and because a value you can read is how you
17339
+ * copy a crop from one cell to another.
17340
+ *
17341
+ * Both write through `clampNormalizedRect` upstream, so neither can hand the
17342
+ * addon a rectangle that leaves the frame — it THROWS on one, by design.
17343
+ */
17344
+ function CropSlider({ label, value, onChange }) {
17345
+ const rounded = Math.round(value * 100) / 100;
17346
+ return /* @__PURE__ */ jsxs("label", {
17347
+ className: "flex flex-col gap-0.5",
17348
+ children: [/* @__PURE__ */ jsxs("span", {
17349
+ className: "flex items-baseline justify-between text-foreground-subtle",
17350
+ children: [/* @__PURE__ */ jsx("span", { children: label }), /* @__PURE__ */ jsx("input", {
17351
+ type: "number",
17352
+ min: 0,
17353
+ max: 1,
17354
+ step: .01,
17355
+ value: rounded,
17356
+ onChange: (event) => {
17357
+ const next = Number(event.target.value);
17358
+ if (Number.isFinite(next)) onChange(next);
17359
+ },
17360
+ className: "w-12 rounded border border-border bg-transparent px-1 py-0.5 text-right"
17361
+ })]
17362
+ }), /* @__PURE__ */ jsx("input", {
17363
+ type: "range",
17364
+ min: 0,
17365
+ max: 1,
17366
+ step: .005,
17367
+ value,
17368
+ onChange: (event) => {
17369
+ const next = Number(event.target.value);
17370
+ if (Number.isFinite(next)) onChange(next);
17371
+ },
17372
+ style: DRAG_SURFACE,
17373
+ className: "h-5 w-full accent-accent"
17374
+ })]
17375
+ });
17376
+ }
17282
17377
  function LabelledNumber({ label, value, step = 1, onChange }) {
17283
17378
  return /* @__PURE__ */ jsxs("label", {
17284
17379
  className: "flex flex-col gap-0.5",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/ui-library",
3
- "version": "1.2.178",
3
+ "version": "1.2.179",
4
4
  "type": "module",
5
5
  "main": "dist/index.cjs",
6
6
  "module": "dist/index.js",