@kolkrabbi/kol-component 0.327.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kolkrabbi/kol-component",
3
- "version": "0.327.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-markdown": "^0.2.0",
31
- "@kolkrabbi/kol-search": "^0.3.0"
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,
@@ -82,6 +82,7 @@ import NavRailSite from './NavRailSite.jsx'
82
82
  * EXPANDS (its rows open under it in place) and COLLAPSES. No other words, in code or docs.
83
83
  *
84
84
  * @param {'app'|'site'|'workshop'} preset which rail (default `app`); `variant="site"` is the old spelling of `preset="site"`
85
+ * @param {'top'} variant `top` — the horizontal top rail (TopRail): logomark · items · `actions` (the right slot)
85
86
  * @param {Array} items `{ icon, path, label, sub?: [{ icon?, path, label }] }`
86
87
  * a section (`sub`) with `to` is a FOLDER NOTE: its icon and label open `to`, its chevron expands or enters
87
88
  * @param {Array} bottomItems pinned below the rule (Settings)
@@ -133,8 +134,8 @@ const openWidth = () => {
133
134
  * a snap reports its target once at the start of the tween (as before) — the sub rows render only while it is true,
134
135
  * because closed they would still take their height and push the rungs below them.
135
136
  *
136
- * ONE AXIS: the gesture reads `pos` and `size` only. A side rail reads x and width; a top rail (variant="top", not
137
- * built) swaps them for y and height — nothing else in here assumes a side. */
137
+ * ONE AXIS: the gesture reads `pos` and `size` only. A side rail reads x and width; a top rail (variant="top" — built
138
+ * 2026-10-31 as `TopRail`, without a drag) would swap them for y and height — nothing else in here assumes a side. */
138
139
  const RAIL_MAX = 560
139
140
  const RAIL_LABELS = 160
140
141
  function useRailDrag(railRef, grabRef, onSnap, snapRef, enabled = true) {
@@ -321,6 +322,7 @@ export const RAIL_PRESETS = {
321
322
  export function registerRailPreset(name, Component) { RAIL_PRESETS[name] = { ...(RAIL_PRESETS[name] ?? {}), component: Component } }
322
323
 
323
324
  export default function NavRail({ variant, preset, ...props }) {
325
+ if (variant === 'top') return <TopRail {...props} />
324
326
  const name = preset ?? (variant === 'site' ? 'site' : 'app')
325
327
  if (name === 'app') return <AppRail {...props} sections={props.sections ?? RAIL_PRESETS.app.sections} />
326
328
  const Named = RAIL_PRESETS[name]?.component
@@ -328,6 +330,37 @@ export default function NavRail({ variant, preset, ...props }) {
328
330
  return <Named {...props} />
329
331
  }
330
332
 
333
+ /* THE TOP RAIL (`variant="top"`, 2026-10-31 — user: *"it would have to be a TopNavRail? and completely rebuilt with Rail
334
+ * Component? so we wouldnt ever import this, but make something in its stead"*). Replaces Atlas's /lab header ("Lab bar"
335
+ * in Figma): the rail's own parts laid out on a row, 48 tall — the mark and the app name (the side rail's logomark row),
336
+ * the items as rail rows (a nav Button rung + the rail label), then `actions` on the right (views / menus — a `Menu` with
337
+ * a `MenuTrigger`). Same type and inks as the side rail; no grab, no drag. A row's `sub` is not drawn here. */
338
+ function TopRail({ items = [], logomark, currentPath = '', onNavigate, iconComponent, actions, hidden = false, className = '' }) {
339
+ if (hidden) return null
340
+ return (
341
+ <header className={`kol-shell-rail kol-shell-rail--top bg-surface-primary sticky top-0 flex h-12 w-full items-center gap-6 px-4 border-b border-fg-08 ${className}`.trim()}>
342
+ {logomark && (
343
+ <div onClick={() => onNavigate?.('/')} className="text-oq-96 cursor-pointer flex items-center gap-3 shrink-0">
344
+ {logomark.svgUrl && <VectorLoader src={logomark.svgUrl} size={20} />}
345
+ <span className="kol-helper-12 uppercase">{logomark.title}</span>
346
+ </div>
347
+ )}
348
+ <nav className="flex min-w-0 flex-1 items-center gap-2 overflow-x-auto">
349
+ {items.map(({ icon, path, label }) => {
350
+ const active = path === '/' ? currentPath === '/' : currentPath.startsWith(path)
351
+ return (
352
+ <div key={path} className="flex items-center gap-2 shrink-0 cursor-pointer" onClick={() => onNavigate?.(path)}>
353
+ {icon && <Button iconOnly={icon} iconSize={RAIL_ROW.glyph} iconComponent={iconComponent} variant="nav" size="md" aria-current={active ? 'page' : undefined} aria-label={label} />}
354
+ <span className={`${RAIL_ROW.label} text-oq-96`}>{label}</span>
355
+ </div>
356
+ )
357
+ })}
358
+ </nav>
359
+ {actions && <div className="flex shrink-0 items-center gap-2">{actions}</div>}
360
+ </header>
361
+ )
362
+ }
363
+
331
364
  function AppRail({
332
365
  items = [],
333
366
  bottomItems = [],