@kolkrabbi/kol-component 0.195.0 → 0.196.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/Input.jsx +17 -1
- package/src/atoms/XYPad.jsx +92 -0
- package/src/index.js +3 -0
- package/src/molecules/InspectorRail.jsx +59 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kolkrabbi/kol-component",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.196.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/atoms/Input.jsx
CHANGED
|
@@ -20,6 +20,12 @@ import { glyphSize } from '../hooks/glyphLadders.js'
|
|
|
20
20
|
* scope expression, not every keystroke. With it
|
|
21
21
|
* the field keeps a local draft seeded from
|
|
22
22
|
* `value`; `onChange` still fires live if given.
|
|
23
|
+
* After the commit the draft RE-SNAPS to `value`,
|
|
24
|
+
* so a rejected commit falls back to the last
|
|
25
|
+
* good value rather than lingering — which makes
|
|
26
|
+
* `type="number"` + `onCommit` the draft/commit
|
|
27
|
+
* number idiom outright (parse and clamp at the
|
|
28
|
+
* call site; fxr's NumberField, retired 2026-09-03).
|
|
23
29
|
* variant="ghost" — legacy alias, resolves to outline
|
|
24
30
|
* variant="property" — the Figma property field (PropertyField,
|
|
25
31
|
* 2026-08-12): filled chrome, dim `affordance`
|
|
@@ -123,7 +129,17 @@ export default function Input({
|
|
|
123
129
|
? {
|
|
124
130
|
value: draft,
|
|
125
131
|
onChange: (e) => { draftRef.current = e.target.value; setDraft(e.target.value); onChange?.(e) },
|
|
126
|
-
|
|
132
|
+
/* RE-SNAP AFTER EVERY COMMIT, not only when `value` changes. The
|
|
133
|
+
* effect above re-syncs the draft on a value change — so a commit the
|
|
134
|
+
* caller REJECTED (invalid input, value kept) left the bad draft on
|
|
135
|
+
* screen, and `1` → `19` → `19x` showed `19x` after blur. kol-fxr's
|
|
136
|
+
* `NumberField` existed for exactly this line (34 lines wrapping this
|
|
137
|
+
* atom: commit, then `setDraft(String(value))`); with the re-snap here
|
|
138
|
+
* it is `<Input type="number" onCommit>` and no component
|
|
139
|
+
* (editor-panels-the-held-specs A8, 2026-09-03). The order matters —
|
|
140
|
+
* commit first, so a caller that DOES accept the value re-renders
|
|
141
|
+
* with the new prop and the effect wins over this fallback. */
|
|
142
|
+
onBlur: () => { onCommit(String(draftRef.current).trim()); draftRef.current = value ?? ''; setDraft(value ?? '') },
|
|
127
143
|
onKeyDown: (e) => {
|
|
128
144
|
if (e.key === 'Enter') e.currentTarget.blur()
|
|
129
145
|
if (e.key === 'Escape') { draftRef.current = value ?? ''; setDraft(value ?? ''); e.currentTarget.blur() }
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { useRef } from 'react'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* XYPad — a two-axis control pad: drag one puck to vary two values at once.
|
|
5
|
+
*
|
|
6
|
+
* Lifted verbatim from kol-fxr's editor (`compose/inspectors/XYPad.jsx`,
|
|
7
|
+
* `editor-panels-the-held-specs` A6, 2026-09-03 — the row the filer marked
|
|
8
|
+
* "portable as-is", and it was: presentation-only, no store coupling). Its own
|
|
9
|
+
* lineage runs back through kol-labs-single's para-type lab to Font Playground.
|
|
10
|
+
*
|
|
11
|
+
* Axis meaning, ranges and the write path belong to the caller. It fills its
|
|
12
|
+
* container's width and is square via `aspect-ratio`; the puck is positioned
|
|
13
|
+
* in %, so there is no `size` prop and a rail of any width takes it. `y` is
|
|
14
|
+
* inverted — top is high — because that is how every axis pad reads.
|
|
15
|
+
*
|
|
16
|
+
* No `useCallback` on the handlers, on purpose: the labs original memoized
|
|
17
|
+
* them with `[]` deps and froze the first render's axis ranges into the drag
|
|
18
|
+
* math, so a pad whose range changed kept mapping to the old one.
|
|
19
|
+
*
|
|
20
|
+
* <XYPad xValue={wdth} yValue={wght} xMin={50} xMax={200} yMin={100} yMax={900}
|
|
21
|
+
* xLabel="Width" yLabel="Weight" onChange={(x, y) => set({ wdth: x, wght: y })} />
|
|
22
|
+
*
|
|
23
|
+
* @param {number} xValue - Current x, in the x range
|
|
24
|
+
* @param {number} yValue - Current y, in the y range
|
|
25
|
+
* @param {number} [xMin=0] - x at the left edge
|
|
26
|
+
* @param {number} [xMax=1] - x at the right edge
|
|
27
|
+
* @param {number} [yMin=0] - y at the BOTTOM edge
|
|
28
|
+
* @param {number} [yMax=1] - y at the top edge
|
|
29
|
+
* @param {Function} onChange - `(x, y) => void` on pointer down and on every move while a button is held — the caller coalesces if it wants one patch per gesture
|
|
30
|
+
* @param {ReactNode} xLabel - Left label above the pad
|
|
31
|
+
* @param {ReactNode} yLabel - Right label above the pad
|
|
32
|
+
* @param {string} [className] - Extra classes on the wrapper
|
|
33
|
+
*/
|
|
34
|
+
export default function XYPad({
|
|
35
|
+
xValue, yValue,
|
|
36
|
+
xMin = 0, xMax = 1,
|
|
37
|
+
yMin = 0, yMax = 1,
|
|
38
|
+
onChange,
|
|
39
|
+
xLabel,
|
|
40
|
+
yLabel,
|
|
41
|
+
className = '',
|
|
42
|
+
}) {
|
|
43
|
+
const ref = useRef(null)
|
|
44
|
+
|
|
45
|
+
const handlePos = (e) => {
|
|
46
|
+
const el = ref.current
|
|
47
|
+
if (!el) return
|
|
48
|
+
const rect = el.getBoundingClientRect()
|
|
49
|
+
const px = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width))
|
|
50
|
+
const py = Math.max(0, Math.min(1, (e.clientY - rect.top) / rect.height))
|
|
51
|
+
const x = xMin + px * (xMax - xMin)
|
|
52
|
+
const y = yMax - py * (yMax - yMin) /* invert: top = high */
|
|
53
|
+
onChange?.(x, y)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const onPointerDown = (e) => {
|
|
57
|
+
e.target.setPointerCapture?.(e.pointerId)
|
|
58
|
+
handlePos(e)
|
|
59
|
+
}
|
|
60
|
+
const onPointerMove = (e) => {
|
|
61
|
+
if (e.buttons === 0) return
|
|
62
|
+
handlePos(e)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const span = (max, min) => (max - min) || 1
|
|
66
|
+
const puckX = ((xValue - xMin) / span(xMax, xMin)) * 100
|
|
67
|
+
const puckY = (1 - (yValue - yMin) / span(yMax, yMin)) * 100
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<div className={`flex flex-col gap-1 ${className}`.trim()}>
|
|
71
|
+
<div className="flex justify-between kol-helper-10 tracking-widest text-meta">
|
|
72
|
+
<span>{xLabel}</span>
|
|
73
|
+
<span>{yLabel}</span>
|
|
74
|
+
</div>
|
|
75
|
+
<div
|
|
76
|
+
ref={ref}
|
|
77
|
+
onPointerDown={onPointerDown}
|
|
78
|
+
onPointerMove={onPointerMove}
|
|
79
|
+
className="relative w-full aspect-square border border-fg-16 bg-fg-04 rounded cursor-crosshair touch-none"
|
|
80
|
+
>
|
|
81
|
+
{/* crosshair guides */}
|
|
82
|
+
<div className="absolute inset-x-0 top-1/2 border-t border-fg-08" />
|
|
83
|
+
<div className="absolute inset-y-0 left-1/2 border-l border-fg-08" />
|
|
84
|
+
{/* puck */}
|
|
85
|
+
<div
|
|
86
|
+
className="absolute w-3 h-3 -ml-1.5 -mt-1.5 rounded-full bg-fg-96 border border-fg-04 pointer-events-none"
|
|
87
|
+
style={{ left: `${puckX}%`, top: `${puckY}%` }}
|
|
88
|
+
/>
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
)
|
|
92
|
+
}
|
package/src/index.js
CHANGED
|
@@ -65,6 +65,9 @@ export { default as ToggleSwitch } from './atoms/ToggleSwitch.jsx'
|
|
|
65
65
|
export { default as TiltCard } from './utilities/TiltCard.jsx'
|
|
66
66
|
export { default as TransparentX } from './utilities/TransparentX.jsx'
|
|
67
67
|
export { default as ViewToggle } from './atoms/ViewToggle.jsx'
|
|
68
|
+
/* the design-editor parts, taken per row from editor-panels-the-held-specs (2026-09-03) */
|
|
69
|
+
export { default as XYPad } from './atoms/XYPad.jsx'
|
|
70
|
+
export { default as InspectorRail } from './molecules/InspectorRail.jsx'
|
|
68
71
|
|
|
69
72
|
// molecules
|
|
70
73
|
export { Accordion, AccordionPanel } from './molecules/Accordion.jsx'
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* InspectorRail — the selection-routing shell of an inspector panel.
|
|
3
|
+
*
|
|
4
|
+
* Lifted from kol-fxr's editor (`compose/InspectorRail.jsx`,
|
|
5
|
+
* `editor-panels-the-held-specs` A7, 2026-09-03) with ALL of its coupling
|
|
6
|
+
* dropped: it read the compose store and imported three concrete panels. What
|
|
7
|
+
* is left is the one piece of logic the filer said everyone gets wrong — the
|
|
8
|
+
* precedence — and it is the thing that makes an inspector rail a component
|
|
9
|
+
* rather than a `<div>`:
|
|
10
|
+
*
|
|
11
|
+
* nothing selected → renders NOTHING (user ruling 2026-08-12: no dummy
|
|
12
|
+
* empty-state copy; selection must visibly spawn
|
|
13
|
+
* its controls)
|
|
14
|
+
* the canvas is selected → `renderers.canvas`, and it WINS over multi-select.
|
|
15
|
+
* Selecting the canvas selects every layer with it,
|
|
16
|
+
* so without this precedence "inspect the canvas"
|
|
17
|
+
* with two layers in frame fell into the multi
|
|
18
|
+
* branch and hid the fill / opacity controls
|
|
19
|
+
* exactly one id → `renderers.single(id)`
|
|
20
|
+
* two or more ids → `renderers.multi(ids)` — the canvas id excluded
|
|
21
|
+
* from the count; it is selectable but not a layer
|
|
22
|
+
*
|
|
23
|
+
* The panels themselves are the consumer's — a layer inspector delegating by
|
|
24
|
+
* type, a canvas inspector, a multi-select summary with a Group action — and
|
|
25
|
+
* they arrive as render functions so this file imports none of them.
|
|
26
|
+
*
|
|
27
|
+
* <InspectorRail
|
|
28
|
+
* selectedIds={selectedIds}
|
|
29
|
+
* canvasId="canvas"
|
|
30
|
+
* renderers={{
|
|
31
|
+
* canvas: () => <CanvasInspector />,
|
|
32
|
+
* single: (id) => <LayerInspector layer={find(id)} />,
|
|
33
|
+
* multi: (ids) => <MultiSummary ids={ids} onGroup={group} />,
|
|
34
|
+
* }}
|
|
35
|
+
* />
|
|
36
|
+
*
|
|
37
|
+
* @param {string[]} selectedIds - The current selection, in selection order; may include `canvasId`
|
|
38
|
+
* @param {string} [canvasId='canvas'] - The id that means "the canvas itself" — precedence, and excluded from the multi count
|
|
39
|
+
* @param {{canvas?: Function, single?: Function, multi?: Function}} renderers - `canvas()`, `single(id)`, `multi(ids)` — each returns the node for that state; a missing renderer renders nothing for it
|
|
40
|
+
* @param {string} [className] - Extra classes on the rail
|
|
41
|
+
*/
|
|
42
|
+
export default function InspectorRail({ selectedIds = [], canvasId = 'canvas', renderers = {}, className = '' }) {
|
|
43
|
+
const isCanvas = selectedIds.includes(canvasId)
|
|
44
|
+
const layerIds = selectedIds.filter((id) => id !== canvasId)
|
|
45
|
+
|
|
46
|
+
const body = isCanvas
|
|
47
|
+
? renderers.canvas?.()
|
|
48
|
+
: layerIds.length >= 2
|
|
49
|
+
? renderers.multi?.(layerIds)
|
|
50
|
+
: layerIds.length === 1
|
|
51
|
+
? renderers.single?.(layerIds[0])
|
|
52
|
+
: null
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<div className={`kol-inspector-rail ${className}`.trim()}>
|
|
56
|
+
{body && <div className="kol-inspector-rail-body">{body}</div>}
|
|
57
|
+
</div>
|
|
58
|
+
)
|
|
59
|
+
}
|