@kolkrabbi/kol-component 0.201.0 → 0.203.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kolkrabbi/kol-component",
3
- "version": "0.201.0",
3
+ "version": "0.203.0",
4
4
  "description": "KOL design-system components — atoms through organisms, emitting canonical kol-* classes. Pairs with @kolkrabbi/kol-theme for styling.",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/src/index.js CHANGED
@@ -130,6 +130,10 @@ export { default as TabsRow } from './molecules/TabsRow.jsx'
130
130
  * it. Absent from this barrel until 0.185.0, which is why the first port of
131
131
  * this set could not keep its chrome screen-constant. */
132
132
  export { default as Canvas, CanvasFrame, PanViewport, PanZoomViewport, CanvasZoomContext, useFps, CANVAS_VIRTUAL_W, DEFAULT_ASPECTS, CANVAS_DEFAULTS } from './organisms/Canvas.jsx'
133
+ /* The rulers and the guides, public since 2026-09-03 (rulers-and-guides-are-private):
134
+ * they measure the RENDERED rect, so they work over a CSS-zoomed stage with no
135
+ * PanZoomViewport anywhere — which is their second consumer on day one. */
136
+ export { CanvasRuler, CanvasGuides, useFrameGeom, niceStep, ticksFor } from './organisms/Canvas.jsx'
133
137
  export { default as EditorShell } from './utilities/EditorShell.jsx'
134
138
  export { default as GalleryCarousel } from './organisms/GalleryCarousel.jsx'
135
139
  export { default as AsciiCursor } from './utilities/AsciiCursor.jsx'
@@ -617,14 +617,13 @@ export function PanZoomViewport({
617
617
  {guides && setGuides && (
618
618
  <CanvasGuides
619
619
  containerRef={containerRef}
620
- view={view}
621
620
  guides={guides}
622
621
  setGuides={setGuides}
623
622
  interactive={guidesInteractive && !spaceHeld}
624
623
  />
625
624
  )}
626
625
 
627
- {showRulers && <CanvasRuler containerRef={containerRef} view={view} disabled={spaceHeld} />}
626
+ {showRulers && <CanvasRuler containerRef={containerRef} disabled={spaceHeld} />}
628
627
 
629
628
  {/* Zoom % + fps — matching chips. Zoom (click resets to 100% /
630
629
  centered) first, fps to its right, shown while `f` toggles it. */}
@@ -659,14 +658,14 @@ const RULER_STEPS = [1, 2, 5, 10, 20, 25, 50, 100, 200, 250, 500, 1000, 2000, 50
659
658
 
660
659
  /* Smallest 1-2-5 virtual step whose on-screen spacing clears `target` px, so
661
660
  * labels never crowd regardless of zoom. */
662
- function niceStep(pxPer, target = 80) {
661
+ export function niceStep(pxPer, target = 80) {
663
662
  for (const s of RULER_STEPS) if (s * pxPer >= target) return s
664
663
  return RULER_STEPS[RULER_STEPS.length - 1]
665
664
  }
666
665
 
667
666
  /* Virtual ticks visible across [0, spanScreen], given where virtual-0 sits on
668
667
  * screen (originScreen) and the screen-px-per-virtual-px scale. */
669
- function ticksFor(originScreen, pxPer, spanScreen, step) {
668
+ export function ticksFor(originScreen, pxPer, spanScreen, step) {
670
669
  const vMin = (0 - originScreen) / pxPer
671
670
  const vMax = (spanScreen - originScreen) / pxPer
672
671
  const first = Math.ceil(vMin / step) * step
@@ -675,6 +674,30 @@ function ticksFor(originScreen, pxPer, spanScreen, step) {
675
674
  return out
676
675
  }
677
676
 
677
+ /**
678
+ * useFrameGeom — the geometry the rulers and the guides both read.
679
+ *
680
+ * PUBLIC SINCE 2026-09-03 (`rulers-and-guides-are-private`, kol-fxr), with
681
+ * `CanvasRuler`, `CanvasGuides`, `niceStep` and `ticksFor`. They were module
682
+ * -private inside `PanZoomViewport` and **they were never tied to it**: this
683
+ * hook locates `[data-canvas-frame]` and reads its `getBoundingClientRect()`
684
+ * against the container, so it measures the RENDERED RESULT and every
685
+ * transform is already folded in. The filer measured it rather than asserting
686
+ * it — CSS `zoom: 0.5` and `transform: scale(0.5)` on a 1920 element both
687
+ * report a 960 rect, so `pxPer` is identical and no branch is needed. A
688
+ * consumer with a CSS-zoomed stage and no pan-zoom viewport at all (a 1920×1080
689
+ * slide editor, its second consumer on day one) gets rulers by rendering these
690
+ * two layers over a container that holds a `[data-canvas-frame]`.
691
+ *
692
+ * `view` is GONE rather than generalised: it never entered the math, and the
693
+ * effect it keyed on already runs a rAF settle-loop that re-measures until the
694
+ * rect is stable for two frames, plus a `ResizeObserver` on the container. Any
695
+ * change is caught by the rect comparison whatever caused it.
696
+ *
697
+ * @param {React.RefObject<HTMLElement>} containerRef - The box the layers are positioned in; must contain a `[data-canvas-frame]`
698
+ * @param {number} [virtualWidth=CANVAS_VIRTUAL_W] - The frame's width in VIRTUAL px — what `pxPer` divides by, and the vertical guide clamp
699
+ * @returns {{left, top, pxPer, vh, cw, ch}|null} frame offsets, screen-px per virtual px, the frame's virtual height, the container's size
700
+ */
678
701
  /* Frame geometry inside the viewport — locates the tagged
679
702
  * `[data-canvas-frame]` and reads its on-screen rect (which already folds in
680
703
  * the letterbox, fit-scale, and the pan/zoom transform) relative to the
@@ -683,7 +706,7 @@ function ticksFor(originScreen, pxPer, spanScreen, step) {
683
706
  * horizontal guides). Re-measures on every `view` change and on container
684
707
  * resize. Shared by CanvasRuler and CanvasGuides so ruler labels and guide
685
708
  * lines can never disagree. */
686
- function useFrameGeom(containerRef, view) {
709
+ export function useFrameGeom(containerRef, virtualWidth = CANVAS_VIRTUAL_W) {
687
710
  const [geom, setGeom] = useState(null)
688
711
 
689
712
  const measure = useCallback(() => {
@@ -693,7 +716,7 @@ function useFrameGeom(containerRef, view) {
693
716
  const crect = el.getBoundingClientRect()
694
717
  if (!frame || crect.width === 0) { setGeom(null); return }
695
718
  const frect = frame.getBoundingClientRect()
696
- const pxPer = frect.width / CANVAS_VIRTUAL_W
719
+ const pxPer = frect.width / virtualWidth
697
720
  setGeom({
698
721
  left: frect.left - crect.left,
699
722
  top: frect.top - crect.top,
@@ -702,7 +725,7 @@ function useFrameGeom(containerRef, view) {
702
725
  cw: crect.width,
703
726
  ch: crect.height,
704
727
  })
705
- }, [containerRef])
728
+ }, [containerRef, virtualWidth])
706
729
 
707
730
  /* A zoom that ANIMATES its transform (a consumer's eased step-zoom) would
708
731
  * have a single measure read the pre-animation rect, and the labels would
@@ -730,7 +753,7 @@ function useFrameGeom(containerRef, view) {
730
753
  }
731
754
  raf = requestAnimationFrame(tick)
732
755
  return () => cancelAnimationFrame(raf)
733
- }, [measure, view, containerRef])
756
+ }, [measure, containerRef])
734
757
 
735
758
  useEffect(() => {
736
759
  const el = containerRef.current
@@ -752,9 +775,16 @@ function useFrameGeom(containerRef, view) {
752
775
  * `kol:zoom-at`) — CanvasGuides owns the guide drag, and the positions live in
753
776
  * the consumer's state. Canvases without a guides layer no-op. `disabled`
754
777
  * (Space-held pan) lets the pointerdown bubble to the pan handler instead.
778
+ *
779
+ * The two layers do not import each other — that event is the whole seam, so a
780
+ * consumer may render either alone.
781
+ *
782
+ * @param {React.RefObject<HTMLElement>} containerRef - The positioned box the bars sit in
783
+ * @param {number} [virtualWidth=CANVAS_VIRTUAL_W] - The frame's virtual width, forwarded to `useFrameGeom`
784
+ * @param {boolean} [disabled=false] - Let a pointerdown bubble instead of starting a guide (a pan gesture is holding the surface)
755
785
  */
756
- function CanvasRuler({ containerRef, view, disabled = false }) {
757
- const geom = useFrameGeom(containerRef, view)
786
+ export function CanvasRuler({ containerRef, virtualWidth = CANVAS_VIRTUAL_W, disabled = false }) {
787
+ const geom = useFrameGeom(containerRef, virtualWidth)
758
788
 
759
789
  const startGuideDrag = (axis) => (e) => {
760
790
  if (disabled || e.button !== 0) return
@@ -863,9 +893,15 @@ function GuideLine({ axis, screenPos, interactive, onGrab }) {
863
893
  * Drags use window-level POINTER events — the ruler cancels its pointerdown,
864
894
  * which suppresses the whole compatibility mouse-event stream for the
865
895
  * interaction, so mousemove/mouseup would never fire.
896
+ *
897
+ * @param {React.RefObject<HTMLElement>} containerRef - The positioned box the lines span
898
+ * @param {number} [virtualWidth=CANVAS_VIRTUAL_W] - The frame's virtual width — also the vertical guides' clamp
899
+ * @param {{h: number[], v: number[]}} guides - Positions in VIRTUAL px; the consumer's state
900
+ * @param {Function} setGuides - Updater, called with the next `{h, v}`
901
+ * @param {boolean} interactive - Allow grab and create; false renders the lines inert
866
902
  */
867
- function CanvasGuides({ containerRef, view, guides, setGuides, interactive }) {
868
- const geom = useFrameGeom(containerRef, view)
903
+ export function CanvasGuides({ containerRef, virtualWidth = CANVAS_VIRTUAL_W, guides, setGuides, interactive }) {
904
+ const geom = useFrameGeom(containerRef, virtualWidth)
869
905
  /* Ref mirror so the drag listeners read fresh geometry without rebinding. */
870
906
  const geomRef = useRef(null)
871
907
  geomRef.current = geom
@@ -908,7 +944,7 @@ function CanvasGuides({ containerRef, view, guides, setGuides, interactive }) {
908
944
  const onUp = (e) => {
909
945
  const pos = posFrom(e)
910
946
  const { axis, index } = guideDrag
911
- const max = axis === 'h' ? Math.round(geomRef.current?.vh ?? 0) : CANVAS_VIRTUAL_W
947
+ const max = axis === 'h' ? Math.round(geomRef.current?.vh ?? 0) : virtualWidth
912
948
  setGuides((g) => {
913
949
  const arr = [...g[axis]]
914
950
  if (pos < 0) {
@@ -928,7 +964,7 @@ function CanvasGuides({ containerRef, view, guides, setGuides, interactive }) {
928
964
  window.removeEventListener('pointermove', onMove)
929
965
  window.removeEventListener('pointerup', onUp)
930
966
  }
931
- }, [guideDrag, toVirtual, setGuides])
967
+ }, [guideDrag, toVirtual, setGuides, virtualWidth])
932
968
 
933
969
  if (!geom || geom.pxPer <= 0) return null
934
970
  const screenFor = (axis, pos) =>