@kolkrabbi/kol-component 0.194.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/src/organisms/MediaLibraryPages.jsx +34 -3
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
|
+
}
|
|
@@ -357,9 +357,37 @@ export function MediaLibraryBrowse({
|
|
|
357
357
|
onSettings={() => setSettingsOpen(true)} />
|
|
358
358
|
|
|
359
359
|
<div className="flex flex-col gap-3">
|
|
360
|
-
{/* Breadcrumb (uppercase, active segment at full ink) · folder-view toggle
|
|
360
|
+
{/* Breadcrumb (uppercase, active segment at full ink) · folder-view toggle
|
|
361
|
+
*
|
|
362
|
+
* AT 390 THIS ROW FAILED (ColumnBrowserStackMode items 3 + 7, kol-r2b2
|
|
363
|
+
* 2026-09-03, measured on a live site): breadcrumb, the folder-view
|
|
364
|
+
* toggle and the ROW·COLUMN pair share one `justify-between` line with
|
|
365
|
+
* no wrap or collapse rule, so the crumbs wrapped to two lines and
|
|
366
|
+
* `COLUMN` was cut off. Below `md` the crumb becomes ONE middle-elided
|
|
367
|
+
* path line and the toggles collapse into the settings drawer — where
|
|
368
|
+
* both already live, so nothing new was minted to hold them. Both
|
|
369
|
+
* references (iOS Files, Dropbox) put these behind a `···` too. */}
|
|
361
370
|
<div className="flex items-center justify-between gap-4">
|
|
362
|
-
|
|
371
|
+
{/* the elided line — first segment, an ellipsis for anything between,
|
|
372
|
+
* and the current one. Below `md` only. */}
|
|
373
|
+
<div className="flex md:hidden items-center gap-2 kol-mono-12 min-w-0">
|
|
374
|
+
<button className={crumbCls(appRoot)} onClick={() => { setAppRoot(true); setPickedFile(null); setPrefix('') }}>{ROOT}</button>
|
|
375
|
+
{!appRoot && bucketMeta.id && crumbs.length > 1 && (
|
|
376
|
+
<><span className="text-oq-32">/</span><span className="text-oq-32">…</span></>
|
|
377
|
+
)}
|
|
378
|
+
{!appRoot && bucketMeta.id && (
|
|
379
|
+
<span className="flex items-center gap-2 min-w-0">
|
|
380
|
+
<span className="text-oq-32">/</span>
|
|
381
|
+
<button
|
|
382
|
+
className={`${crumbCls(true)} truncate`}
|
|
383
|
+
onClick={() => setPrefix(crumbs.length ? crumbs.join('/') + '/' : '')}
|
|
384
|
+
>
|
|
385
|
+
{(crumbs[crumbs.length - 1] ?? label).toUpperCase()}
|
|
386
|
+
</button>
|
|
387
|
+
</span>
|
|
388
|
+
)}
|
|
389
|
+
</div>
|
|
390
|
+
<div className="hidden md:flex items-center gap-2 kol-mono-12">
|
|
363
391
|
<button className={crumbCls(appRoot)} onClick={() => { setAppRoot(true); setPickedFile(null); setPrefix('') }}>{ROOT}</button>
|
|
364
392
|
{!appRoot && bucketMeta.id && (
|
|
365
393
|
<span className="flex items-center gap-2">
|
|
@@ -381,7 +409,10 @@ export function MediaLibraryBrowse({
|
|
|
381
409
|
<span className="flex items-center gap-2"><span className="text-oq-32">/</span><span className="text-oq-96">{pickedFile.key.slice(prefix.length).toUpperCase()}</span></span>
|
|
382
410
|
)}
|
|
383
411
|
</div>
|
|
384
|
-
|
|
412
|
+
{/* the control cluster is desktop-only — below `md` these two toggles
|
|
413
|
+
* are the same setting the drawer already carries, and nothing in
|
|
414
|
+
* this row survives 390 */}
|
|
415
|
+
<div className="hidden md:flex items-center gap-6">
|
|
385
416
|
<ViewToggle viewMode={folderView} onViewChange={(v) => setSettings({ ...settings, folderView: v })} variant="icon" options={FOLDER_VIEW_OPTIONS} />
|
|
386
417
|
<Divider variant="vertical" />
|
|
387
418
|
<div className="flex items-center gap-4">
|