@kolkrabbi/kol-component 0.328.0 → 0.329.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 +3 -3
- package/src/atoms/OriginPicker.jsx +111 -0
- package/src/index.js +2 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kolkrabbi/kol-component",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.329.0",
|
|
4
4
|
"description": "The core component library",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -27,8 +27,8 @@
|
|
|
27
27
|
"pdfjs-dist": "^6.3.289",
|
|
28
28
|
"react-syntax-highlighter": "^16.1.1",
|
|
29
29
|
"@kolkrabbi/kol-growth": "^0.4.0",
|
|
30
|
-
"@kolkrabbi/kol-
|
|
31
|
-
"@kolkrabbi/kol-
|
|
30
|
+
"@kolkrabbi/kol-search": "^0.3.0",
|
|
31
|
+
"@kolkrabbi/kol-markdown": "^0.2.0"
|
|
32
32
|
},
|
|
33
33
|
"peerDependencies": {
|
|
34
34
|
"@kolkrabbi/kol-icons": ">=0.22.0",
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { useRef } from 'react'
|
|
2
|
+
import { toneClass } from '../utilities/tone.js'
|
|
3
|
+
|
|
4
|
+
const STEPS = [0, 0.5, 1]
|
|
5
|
+
const X_NAME = ['left', 'center', 'right']
|
|
6
|
+
const Y_NAME = ['top', 'center', 'bottom']
|
|
7
|
+
const pointName = (xi, yi) => (xi === 1 && yi === 1 ? 'center' : `${X_NAME[xi]} ${Y_NAME[yi]}`)
|
|
8
|
+
|
|
9
|
+
/* box padding + cell + dot per rung: 2·pad + 3·cell = the field's height (22 · 26 · 32; touch 32 · 32 · 36).
|
|
10
|
+
* The xs touch lift skips a hardware panel, as Input's does. */
|
|
11
|
+
const SIZE = {
|
|
12
|
+
xs: 'p-[2px] [--op-cell:6px] [--op-dot:4px] rounded-[var(--kol-radius-xs)] pointer-coarse:not-[.kol-hw-panel_*]:p-px pointer-coarse:not-[.kol-hw-panel_*]:[--op-cell:10px] pointer-coarse:not-[.kol-hw-panel_*]:[--op-dot:6px]',
|
|
13
|
+
sm: 'p-px [--op-cell:8px] [--op-dot:4px] rounded-[var(--kol-radius-sm)] pointer-coarse:[--op-cell:10px] pointer-coarse:[--op-dot:6px]',
|
|
14
|
+
md: 'p-px [--op-cell:10px] [--op-dot:6px] rounded-[var(--kol-radius-sm)] pointer-coarse:p-[3px]',
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* OriginPicker — Pick one of nine anchor points. a 3×3 transform-origin / anchor value picker.
|
|
19
|
+
*
|
|
20
|
+
* Lobby OriginPicker (kol-fxr, 2026-10-31): fxr's layer inspector hand-built it as `AnchorPicker`
|
|
21
|
+
* (nine 6 px radio dots on surface-secondary) beside its X / Y fields. `AlignmentGrid` is not it —
|
|
22
|
+
* that fires align actions and holds no value; this holds one.
|
|
23
|
+
*
|
|
24
|
+
* ON THE CONTROL RAMP. The square is exactly a field's height at the same `size`, so it sits beside
|
|
25
|
+
* an `Input` without a step: xs 22 · sm 26 · md 32 (touch 32 · 32 · 36, as the fields grow). The
|
|
26
|
+
* ground is the control's (`--kol-tone-bg`, else surface-secondary), so `tone` and a toned wrapper
|
|
27
|
+
* reach it as they reach an Input. No border — the field family has none.
|
|
28
|
+
*
|
|
29
|
+
* INKS (the icon ink law — oq, never fg): a point rests at oq-24, a hovered one lifts to oq-48, the
|
|
30
|
+
* selected one is the accent. Focus is the segmented strip's quiet ring, drawn on the square.
|
|
31
|
+
*
|
|
32
|
+
* A11y: a radiogroup of nine radios with a roving tabindex — Tab lands on the selected point,
|
|
33
|
+
* ←/→ move x and ↑/↓ move y (selection and focus together, no wrap).
|
|
34
|
+
*
|
|
35
|
+
* <OriginPicker value={anchor} onChange={setAnchor} ariaLabel="Transform origin" size="sm" />
|
|
36
|
+
*
|
|
37
|
+
* @param {[number, number]} value - The selected point `[x, y]`, each 0 · 0.5 · 1 (0 = left / top)
|
|
38
|
+
* @param {(value:[number, number]) => void} onChange - Fired with the new `[x, y]` on click or arrow key
|
|
39
|
+
* @param {'xs'|'sm'|'md'} [size='sm'] - The control rung — the square matches a field of this size
|
|
40
|
+
* @param {string} [tone='default'] - The control set's tone (the ground); default inherits a wrapper's
|
|
41
|
+
* @param {string} [ariaLabel='Origin'] - The group's accessible name
|
|
42
|
+
* @param {string} [className] - Extra classes on the square
|
|
43
|
+
*/
|
|
44
|
+
export default function OriginPicker({
|
|
45
|
+
value = [0.5, 0.5],
|
|
46
|
+
onChange,
|
|
47
|
+
size = 'sm',
|
|
48
|
+
tone = 'default',
|
|
49
|
+
ariaLabel = 'Origin',
|
|
50
|
+
className = '',
|
|
51
|
+
}) {
|
|
52
|
+
const refs = useRef([])
|
|
53
|
+
const sel = [STEPS.indexOf(value?.[0]), STEPS.indexOf(value?.[1])]
|
|
54
|
+
|
|
55
|
+
const pick = (xi, yi, focus) => {
|
|
56
|
+
onChange?.([STEPS[xi], STEPS[yi]])
|
|
57
|
+
if (focus) refs.current[yi * 3 + xi]?.focus()
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const onKeyDown = (e, xi, yi) => {
|
|
61
|
+
const d = { ArrowLeft: [-1, 0], ArrowRight: [1, 0], ArrowUp: [0, -1], ArrowDown: [0, 1] }[e.key]
|
|
62
|
+
if (!d) return
|
|
63
|
+
e.preventDefault()
|
|
64
|
+
const nx = Math.min(2, Math.max(0, xi + d[0]))
|
|
65
|
+
const ny = Math.min(2, Math.max(0, yi + d[1]))
|
|
66
|
+
if (nx !== xi || ny !== yi) pick(nx, ny, true)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/* nothing valid selected → the centre takes the tab stop, so the group is still reachable */
|
|
70
|
+
const tabStop = sel[0] < 0 || sel[1] < 0 ? [1, 1] : sel
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<div
|
|
74
|
+
role="radiogroup"
|
|
75
|
+
aria-label={ariaLabel}
|
|
76
|
+
className={[
|
|
77
|
+
'kol-origin-picker inline-grid grid-cols-3 shrink-0 bg-[var(--kol-tone-bg,var(--kol-surface-secondary))]',
|
|
78
|
+
'has-[:focus-visible]:outline has-[:focus-visible]:outline-1 has-[:focus-visible]:outline-[var(--kol-focus-ring-quiet)] has-[:focus-visible]:-outline-offset-2',
|
|
79
|
+
SIZE[size] ?? SIZE.sm,
|
|
80
|
+
toneClass(tone),
|
|
81
|
+
className,
|
|
82
|
+
].filter(Boolean).join(' ')}
|
|
83
|
+
>
|
|
84
|
+
{STEPS.map((_, yi) => STEPS.map((__, xi) => {
|
|
85
|
+
const on = sel[0] === xi && sel[1] === yi
|
|
86
|
+
const stop = tabStop[0] === xi && tabStop[1] === yi
|
|
87
|
+
return (
|
|
88
|
+
<button
|
|
89
|
+
key={`${xi}${yi}`}
|
|
90
|
+
ref={(el) => { refs.current[yi * 3 + xi] = el }}
|
|
91
|
+
type="button"
|
|
92
|
+
role="radio"
|
|
93
|
+
aria-checked={on}
|
|
94
|
+
aria-label={pointName(xi, yi)}
|
|
95
|
+
tabIndex={stop ? 0 : -1}
|
|
96
|
+
onClick={() => pick(xi, yi, false)}
|
|
97
|
+
onKeyDown={(e) => onKeyDown(e, xi, yi)}
|
|
98
|
+
className={`group grid place-items-center p-0 border-0 bg-transparent cursor-pointer outline-none size-[var(--op-cell)]`}
|
|
99
|
+
>
|
|
100
|
+
<span
|
|
101
|
+
aria-hidden="true"
|
|
102
|
+
className={`block rounded-full transition-colors size-[var(--op-dot)] ${on
|
|
103
|
+
? 'bg-[var(--kol-accent-primary)]'
|
|
104
|
+
: 'bg-[var(--kol-oq-24)] group-hover:bg-[var(--kol-oq-48)]'}`}
|
|
105
|
+
/>
|
|
106
|
+
</button>
|
|
107
|
+
)
|
|
108
|
+
}))}
|
|
109
|
+
</div>
|
|
110
|
+
)
|
|
111
|
+
}
|
package/src/index.js
CHANGED
|
@@ -83,6 +83,8 @@ export { default as TransparentX } from './utilities/TransparentX.jsx'
|
|
|
83
83
|
export { default as ViewToggle } from './atoms/ViewToggle.jsx'
|
|
84
84
|
/* the design-editor parts, taken per row from editor-panels-the-held-specs (2026-09-03) */
|
|
85
85
|
export { default as XYPad } from './atoms/XYPad.jsx'
|
|
86
|
+
/* lobby OriginPicker (kol-fxr, 2026-10-31) — the 3×3 origin / anchor value picker */
|
|
87
|
+
export { default as OriginPicker } from './atoms/OriginPicker.jsx'
|
|
86
88
|
export { default as InspectorRail } from './molecules/InspectorRail.jsx'
|
|
87
89
|
/* SelectionOverlay's two siblings — same 1080-virtual contract, same zoom
|
|
88
90
|
* division (editor-panels-the-held-specs B2). `pathMath` is their geometry,
|