@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 +1 -1
- package/src/index.js +4 -0
- package/src/organisms/Canvas.jsx +50 -14
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kolkrabbi/kol-component",
|
|
3
|
-
"version": "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'
|
package/src/organisms/Canvas.jsx
CHANGED
|
@@ -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}
|
|
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,
|
|
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 /
|
|
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,
|
|
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,
|
|
757
|
-
const geom = useFrameGeom(containerRef,
|
|
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,
|
|
868
|
-
const geom = useFrameGeom(containerRef,
|
|
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) :
|
|
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) =>
|