@kolkrabbi/kol-component 0.184.0 → 0.185.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/atoms/SelectionOverlay.jsx +83 -17
- package/src/hooks/canvasZoom.js +19 -0
- package/src/index.js +5 -1
- package/src/organisms/Canvas.jsx +9 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kolkrabbi/kol-component",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.185.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",
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { useContext } from 'react'
|
|
2
|
+
import { CanvasZoomContext } from '../hooks/canvasZoom.js'
|
|
3
|
+
|
|
1
4
|
/* taxonomy-ok: presentational transform-chrome overlay. It nests no KOL
|
|
2
5
|
* component (pure inline-styled squares + label), so by the letter of the
|
|
3
6
|
* molecule test it reads as an atom — but the lobby spec places it as a
|
|
@@ -7,23 +10,40 @@
|
|
|
7
10
|
/**
|
|
8
11
|
* SelectionOverlay — pure transform chrome for a selected box.
|
|
9
12
|
*
|
|
10
|
-
* Renders a dashed outline, 8 named resize handles,
|
|
11
|
-
* label, all positioned in the **same 1080-virtual
|
|
12
|
-
* target lives in (pairs with Canvas's scale layer —
|
|
13
|
-
* the box inside the same scale layer). Each handle
|
|
14
|
-
* `data-handle="NW|N|NE|E|SE|S|SW|W"` attribute so a parent's
|
|
15
|
-
* can start the right
|
|
16
|
-
* math lives in the consumer, which reads
|
|
13
|
+
* Renders a dashed outline, 8 named resize handles, a rotate handle and a
|
|
14
|
+
* `W × H` dimension label, all positioned in the **same 1080-virtual
|
|
15
|
+
* coordinate space** the target lives in (pairs with Canvas's scale layer —
|
|
16
|
+
* place it as a sibling of the box inside the same scale layer). Each handle
|
|
17
|
+
* carries a `data-handle="NW|N|NE|E|SE|S|SW|W|ROT"` attribute so a parent's
|
|
18
|
+
* pointer router can start the right drag mode. No interaction logic of its
|
|
19
|
+
* own — the drag math lives in the consumer, which reads
|
|
20
|
+
* `e.target.dataset.handle`.
|
|
17
21
|
*
|
|
18
|
-
* Ported from
|
|
22
|
+
* Ported from kol-fxr's editor with the `layer` model reduced to a flat `box`
|
|
19
23
|
* (per lobby spec): renders nothing when there's no positional box.
|
|
20
24
|
*
|
|
21
|
-
*
|
|
25
|
+
* ZOOM COMPENSATION IS THE POINT (restored 2026-09-03,
|
|
26
|
+
* `editor-set-is-behind-its-source`). The chrome renders in virtual px INSIDE
|
|
27
|
+
* the canvas's zoomed transform, so every screen-constant dimension — handle
|
|
28
|
+
* size, outline width, the rotate handle's offset, the label — divides by the
|
|
29
|
+
* live zoom from `CanvasZoomContext`. The first port hardcoded `1px` / `10px`
|
|
30
|
+
* / `marginTop: 6`, so at 3× the handles drew 30px and the label ballooned;
|
|
31
|
+
* kol-fxr measured it and reverted the adoption. Outside a `PanZoomViewport`
|
|
32
|
+
* the context is 1 and every division is a no-op, so a static canvas is
|
|
33
|
+
* unaffected.
|
|
34
|
+
*
|
|
35
|
+
* The label also counter-SCALES rather than just re-sizing: `scale(1/zoom)`
|
|
36
|
+
* with a top-left origin keeps its padding, radius and letter-spacing
|
|
37
|
+
* screen-constant too, which a font-size alone does not.
|
|
38
|
+
*
|
|
39
|
+
* @param {{x:number,y:number,w:number,h:number,rotation?:number}} box virtual-coord position + size; null/x==null → renders nothing. `rotation` in degrees turns the chrome with the box about its centre
|
|
22
40
|
* @param {boolean} showHandles render the 8 resize handles (default true)
|
|
41
|
+
* @param {boolean} showRotate render the rotate handle (default: follows `showHandles`) — independent because a path hides the resize handles, node-edit owning their geometry, and still rotates
|
|
23
42
|
* @param {boolean} showLabel render the `W × H` dimension label (default true)
|
|
24
|
-
* @param {number} handleSize handle square size in virtual px (default 10)
|
|
43
|
+
* @param {number} handleSize handle square size in virtual px BEFORE zoom compensation (default 10)
|
|
25
44
|
* @param {string} accentColor outline + handle + label color (default var(--kol-accent-primary))
|
|
26
45
|
* @param {Function} labelFormatter (box) => string — dimension readout (default `${round(w)} × ${round(h)}`)
|
|
46
|
+
* @param {string} rotateTitle tooltip on the rotate handle (default 'Rotate')
|
|
27
47
|
*/
|
|
28
48
|
const HANDLE_DIRS = [
|
|
29
49
|
{ dir: 'NW', cursor: 'nwse-resize', x: 0, y: 0 },
|
|
@@ -36,17 +56,35 @@ const HANDLE_DIRS = [
|
|
|
36
56
|
{ dir: 'W', cursor: 'ew-resize', x: 0, y: 0.5 },
|
|
37
57
|
]
|
|
38
58
|
|
|
59
|
+
/* The rotate handle's float above the top edge, in virtual px before zoom
|
|
60
|
+
* compensation — fxr's number. */
|
|
61
|
+
const ROTATE_OFFSET = 22
|
|
62
|
+
|
|
39
63
|
export default function SelectionOverlay({
|
|
40
64
|
box,
|
|
41
65
|
showHandles = true,
|
|
66
|
+
showRotate,
|
|
42
67
|
showLabel = true,
|
|
43
68
|
handleSize = 10,
|
|
44
69
|
accentColor = 'var(--kol-accent-primary)',
|
|
45
70
|
labelFormatter = (b) => `${Math.round(b.w)} × ${Math.round(b.h)}`,
|
|
71
|
+
rotateTitle = 'Rotate',
|
|
46
72
|
}) {
|
|
73
|
+
/* Chrome renders in virtual px inside the zoomed transform — divide by zoom
|
|
74
|
+
* so handles / outline / label stay screen-constant at any zoom. 1 outside a
|
|
75
|
+
* PanZoomViewport, which makes every division below a no-op. */
|
|
76
|
+
const zoom = useContext(CanvasZoomContext)
|
|
77
|
+
|
|
47
78
|
if (!box || box.x == null) return null /* no positional box → no chrome */
|
|
48
79
|
|
|
49
80
|
const { x, y, w, h } = box
|
|
81
|
+
const size = handleSize / zoom
|
|
82
|
+
const hairline = 1 / zoom
|
|
83
|
+
const rotate = showRotate ?? showHandles
|
|
84
|
+
/* `rotation` may arrive as a BINDING OBJECT on an animated prop; chrome uses
|
|
85
|
+
* the base 0 rather than throwing on `${{…}}deg` — editing chrome over
|
|
86
|
+
* animated transforms is a consumer-side v1 limitation, and fxr's guard. */
|
|
87
|
+
const rot = typeof box.rotation === 'number' ? box.rotation : 0
|
|
50
88
|
|
|
51
89
|
return (
|
|
52
90
|
<div
|
|
@@ -54,6 +92,9 @@ export default function SelectionOverlay({
|
|
|
54
92
|
position: 'absolute',
|
|
55
93
|
left: x, top: y,
|
|
56
94
|
width: w, height: h,
|
|
95
|
+
/* the chrome rotates WITH the box (centre origin) so the wireframe and
|
|
96
|
+
* the handles hug the actually-rendered box, not its unrotated slot */
|
|
97
|
+
transform: rot ? `rotate(${rot}deg)` : undefined,
|
|
57
98
|
pointerEvents: 'none',
|
|
58
99
|
zIndex: 100,
|
|
59
100
|
}}
|
|
@@ -61,22 +102,43 @@ export default function SelectionOverlay({
|
|
|
61
102
|
<div
|
|
62
103
|
style={{
|
|
63
104
|
position: 'absolute', inset: 0,
|
|
64
|
-
outline:
|
|
105
|
+
outline: `${hairline}px dashed ${accentColor}`,
|
|
65
106
|
outlineOffset: 0,
|
|
66
107
|
}}
|
|
67
108
|
/>
|
|
109
|
+
{/* rotate handle — a circle floating above the top edge; a drag rotates
|
|
110
|
+
* the box about its centre. Independent of the resize handles: a path
|
|
111
|
+
* hides those (node-edit owns their geometry) and still rotates. */}
|
|
112
|
+
{rotate && (
|
|
113
|
+
<div
|
|
114
|
+
data-handle="ROT"
|
|
115
|
+
title={rotateTitle}
|
|
116
|
+
style={{
|
|
117
|
+
position: 'absolute',
|
|
118
|
+
left: `calc(50% - ${size / 2}px)`,
|
|
119
|
+
top: -(ROTATE_OFFSET / zoom),
|
|
120
|
+
width: size,
|
|
121
|
+
height: size,
|
|
122
|
+
borderRadius: '50%',
|
|
123
|
+
background: 'white',
|
|
124
|
+
border: `${hairline}px solid ${accentColor}`,
|
|
125
|
+
cursor: 'grab',
|
|
126
|
+
pointerEvents: 'auto',
|
|
127
|
+
}}
|
|
128
|
+
/>
|
|
129
|
+
)}
|
|
68
130
|
{showHandles && HANDLE_DIRS.map(({ dir, cursor, x: hx, y: hy }) => (
|
|
69
131
|
<div
|
|
70
132
|
key={dir}
|
|
71
133
|
data-handle={dir}
|
|
72
134
|
style={{
|
|
73
135
|
position: 'absolute',
|
|
74
|
-
left: `calc(${hx * 100}% - ${
|
|
75
|
-
top: `calc(${hy * 100}% - ${
|
|
76
|
-
width:
|
|
77
|
-
height:
|
|
136
|
+
left: `calc(${hx * 100}% - ${size / 2}px)`,
|
|
137
|
+
top: `calc(${hy * 100}% - ${size / 2}px)`,
|
|
138
|
+
width: size,
|
|
139
|
+
height: size,
|
|
78
140
|
background: 'white',
|
|
79
|
-
border:
|
|
141
|
+
border: `${hairline}px solid ${accentColor}`,
|
|
80
142
|
cursor,
|
|
81
143
|
pointerEvents: 'auto',
|
|
82
144
|
}}
|
|
@@ -88,7 +150,11 @@ export default function SelectionOverlay({
|
|
|
88
150
|
position: 'absolute',
|
|
89
151
|
left: 0,
|
|
90
152
|
top: '100%',
|
|
91
|
-
marginTop: 6,
|
|
153
|
+
marginTop: 6 / zoom,
|
|
154
|
+
/* counter-scale, not just a smaller font: padding, radius and
|
|
155
|
+
* tracking have to stay screen-constant too */
|
|
156
|
+
transform: `scale(${1 / zoom})`,
|
|
157
|
+
transformOrigin: 'top left',
|
|
92
158
|
fontFamily: 'var(--kol-font-family-mono)',
|
|
93
159
|
fontSize: 10,
|
|
94
160
|
letterSpacing: '0.04em',
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { createContext } from 'react'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* CanvasZoomContext — the canvas viewport's live zoom factor.
|
|
5
|
+
*
|
|
6
|
+
* Lives in `src/hooks` rather than beside `Canvas` because BOTH tiers need it:
|
|
7
|
+
* the organism publishes it (`PanZoomViewport`) and the atoms consume it
|
|
8
|
+
* (`SelectionOverlay` divides every screen-constant dimension by it). An atom
|
|
9
|
+
* importing `../organisms/Canvas.jsx` is an upward import and the taxonomy
|
|
10
|
+
* gate is right to refuse it — so the shared value moves down to the tier
|
|
11
|
+
* neither side owns, the same reason `glyphLadders.js` sits here.
|
|
12
|
+
*
|
|
13
|
+
* Defaults to 1, so a consumer reads it unconditionally and a canvas without a
|
|
14
|
+
* pan-zoom viewport turns every `/ zoom` into a no-op.
|
|
15
|
+
*
|
|
16
|
+
* `@kolkrabbi/kol-component` exports it from the barrel as `CanvasZoomContext`,
|
|
17
|
+
* and `Canvas.jsx` re-exports it under the same name it always had.
|
|
18
|
+
*/
|
|
19
|
+
export const CanvasZoomContext = createContext(1)
|
package/src/index.js
CHANGED
|
@@ -111,7 +111,11 @@ export { default as TabsRow } from './molecules/TabsRow.jsx'
|
|
|
111
111
|
/* monorepo sets (P6–P10) — organism members. Foundry members live in the
|
|
112
112
|
standalone @kolkrabbi/kol-foundry package (with the type-specimen kit +
|
|
113
113
|
live-font effects moved there 2026-07-09) — never re-exported here. */
|
|
114
|
-
|
|
114
|
+
/* `CanvasZoomContext` and `PanZoomViewport` are the load-bearing pair for an
|
|
115
|
+
* editor: the viewport publishes the zoom, every piece of editing chrome reads
|
|
116
|
+
* it. Absent from this barrel until 0.185.0, which is why the first port of
|
|
117
|
+
* this set could not keep its chrome screen-constant. */
|
|
118
|
+
export { default as Canvas, CanvasFrame, PanViewport, PanZoomViewport, CanvasZoomContext, useFps, CANVAS_VIRTUAL_W, DEFAULT_ASPECTS, CANVAS_DEFAULTS } from './organisms/Canvas.jsx'
|
|
115
119
|
export { default as EditorShell } from './utilities/EditorShell.jsx'
|
|
116
120
|
export { default as GalleryCarousel } from './organisms/GalleryCarousel.jsx'
|
|
117
121
|
export { default as AsciiCursor } from './utilities/AsciiCursor.jsx'
|
package/src/organisms/Canvas.jsx
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useCallback, useContext, useEffect, useLayoutEffect, useRef, useState } from 'react'
|
|
2
|
+
import { CanvasZoomContext } from '../hooks/canvasZoom.js'
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Canvas — the editor's aspect-ratio stage.
|
|
@@ -31,11 +32,13 @@ import { createContext, useCallback, useEffect, useLayoutEffect, useRef, useStat
|
|
|
31
32
|
* pan-only viewport is not this component, it is a third of it.
|
|
32
33
|
*/
|
|
33
34
|
|
|
34
|
-
/*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
|
|
35
|
+
/* The viewport's live zoom factor — editing chrome divides its virtual-px
|
|
36
|
+
* dimensions by it to stay screen-constant. Defined in `hooks/canvasZoom.js`
|
|
37
|
+
* because the atoms consume it and an atom may not import an organism (the
|
|
38
|
+
* taxonomy gate, correctly); re-exported here under the name it has always
|
|
39
|
+
* had, so `import { CanvasZoomContext } from '@kolkrabbi/kol-component'` and
|
|
40
|
+
* every existing deep import keep working. */
|
|
41
|
+
export { CanvasZoomContext }
|
|
39
42
|
|
|
40
43
|
/* Fixed virtual canvas width — children render in this pixel space and the
|
|
41
44
|
* outer rect scales to fit the viewport via CSS transform. Height is derived
|