@kolkrabbi/kol-component 0.190.0 → 0.191.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.190.0",
3
+ "version": "0.191.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",
@@ -72,6 +72,7 @@ import useScrollSpy from '../hooks/useScrollSpy.js'
72
72
  * @param {Element} root IntersectionObserver root passed to the spy
73
73
  * @param {number} minors RAIL: graduations between one heading and the next — the dial's resolution, so literally how many clicks a heading is worth (default 10)
74
74
  * @param {number} falloff RAIL: the gaussian's sigma in px, measured against the TICK pitch not the label pitch — at 10 graduations of 3px a section spans ~52px, so 14 crosses four or five clicks. Widen it and the whole column swells together, which is a column getting bigger rather than a lens moving over it (default 14)
75
+ * @param {'tick'|'label'|'off'} snap RAIL: what the lens locks to — `tick` (default) is the safecracker dial, locking to the nearest graduation and holding across its band; `label` locks to headings only, which is the ONLY sensible detent at `minors={0}`; `off` lets the lens follow the pointer continuously, which is the honest fisheye and reads better on a long rail with no graduations
75
76
  * @param {'left'|'right'} position RAIL: which edge it pins to (default 'right')
76
77
  * @param {number} minSections RAIL: render nothing under this many headings — an index of one is noise (default 2)
77
78
  * @param {string} ariaLabel RAIL: the nav's accessible name (default 'On this page')
@@ -85,6 +86,7 @@ export default function DocsToc({
85
86
  root = null,
86
87
  minors = 10,
87
88
  falloff = 14,
89
+ snap = 'tick',
88
90
  position = 'right',
89
91
  minSections = 2,
90
92
  ariaLabel = 'On this page',
@@ -100,6 +102,7 @@ export default function DocsToc({
100
102
  onNavigate={onNavigate}
101
103
  minors={minors}
102
104
  falloff={falloff}
105
+ snap={snap}
103
106
  position={position}
104
107
  minSections={minSections}
105
108
  ariaLabel={ariaLabel}
@@ -145,7 +148,7 @@ export default function DocsToc({
145
148
  * do not belong in the other's render path.
146
149
  *
147
150
  * The magnitudes go to the DOM as `--m`, never to state — see the header. */
148
- function TocRail({ toc, activeId, onNavigate, minors, falloff, position, minSections, ariaLabel, className }) {
151
+ function TocRail({ toc, activeId, onNavigate, minors, falloff, snap, position, minSections, ariaLabel, className }) {
149
152
  const navRef = useRef(null)
150
153
  const centresRef = useRef([])
151
154
  const frameRef = useRef(0)
@@ -166,19 +169,38 @@ function TocRail({ toc, activeId, onNavigate, minors, falloff, position, minSect
166
169
  const nav = navRef.current
167
170
  if (!nav) return
168
171
 
169
- /* DETENT, on EVERY graduation — the safecracker's dial. The lens locks to
170
- * the nearest mark and holds across that mark's whole band, so dragging
171
- * down the rail is a run of discrete clicks rather than a smear. */
172
+ /* THE DETENT. `tick` is the safecracker's dial the lens locks to the
173
+ * nearest mark and holds across its whole band, so dragging down the rail
174
+ * is a run of discrete clicks rather than a smear. The three modes differ
175
+ * only in WHICH array is snapped against, which is why this is a prop and
176
+ * not three components (`docstoc-rail-tracking-and-snap-prop`, 2026-09-04):
177
+ *
178
+ * tick every graduation — the default, right for an index you aim with
179
+ * label the headings only — coarse at 10 minors, and the ONLY sensible
180
+ * detent at `minors={0}`, which the component already allows: no
181
+ * ticks plus a tick-snap is `label` by accident rather than choice
182
+ * off no snap at all — the honest fisheye, and better on a long rail
183
+ * with many headings and no graduations
184
+ *
185
+ * Majors are every (minors + 1)th child, because the render interleaves
186
+ * one heading with `minors` ticks and stops after the last heading. */
172
187
  const marks = centresRef.current
173
- const focus = y == null || !marks.length
188
+ const targets = snap === 'off'
174
189
  ? null
175
- : marks.reduce((best, c) => (Math.abs(c - y) < Math.abs(best - y) ? c : best), marks[0])
190
+ : snap === 'label'
191
+ ? marks.filter((_, i) => i % (minors + 1) === 0)
192
+ : marks
193
+ const focus = y == null
194
+ ? null
195
+ : !targets?.length
196
+ ? y
197
+ : targets.reduce((best, c) => (Math.abs(c - y) < Math.abs(best - y) ? c : best), targets[0])
176
198
 
177
199
  marks.forEach((c, i) => {
178
200
  const m = focus == null ? 0 : Math.exp(-(((focus - c) / falloff) ** 2))
179
201
  nav.children[i]?.style.setProperty('--m', m.toFixed(3))
180
202
  })
181
- }, [falloff])
203
+ }, [falloff, snap, minors])
182
204
 
183
205
  const onMove = useCallback((e) => {
184
206
  /* Reduced motion: the curve is the animation, so there is nothing to damp —
@@ -228,13 +250,28 @@ function TocRail({ toc, activeId, onNavigate, minors, falloff, position, minSect
228
250
  data-active={active ? '' : undefined}
229
251
  className="kol-toc-row"
230
252
  >
231
- {/* ONE rail voice (validate:rails R1). The filer's fork ran
232
- * `kol-helper-12`, which is a second row ramp inside a rail and
233
- * exactly what that gate exists to stop the rung is the rail's,
234
- * and the fisheye scales FROM it rather than replacing it. If 14
235
- * proves too heavy under the lens, that is a change to the rail
236
- * law and belongs in the docs before it belongs here. */}
237
- <span className="kol-toc-label kol-mono-14 text-meta">{label}</span>
253
+ {/* `kol-helper-12`, and it is the FAULT LINE that says so, not
254
+ * taste (01-foundations/03-typography: *"can this string ever
255
+ * wrap? No (structurally single-line) helper"*). This label is
256
+ * `white-space: nowrap` a graduation on a ruler, which is the
257
+ * helper family's own listed use.
258
+ *
259
+ * The line-height is the functional half. `kol-mono-14` carries
260
+ * 18px leading on a 14px label: 4px this string never uses, and
261
+ * THIS component measures its rows — `measure()` reads
262
+ * `getBoundingClientRect()` on every child to place the detents,
263
+ * so leading inflates each row box and moves the label's centre
264
+ * off its own tick. A ramp built for wrapping text is the wrong
265
+ * tool for a mark on a ruler.
266
+ *
267
+ * It shipped as `kol-mono-14` in 0.188.0 because validate:rails
268
+ * R1 tested the class PREFIX rather than the rung's purpose and I
269
+ * took the gate's word over the type law. The filer had specified
270
+ * helper-12 deliberately (user: *"its a bit big and tight and
271
+ * bold"*, and then *"I just dont get the point of me desiging for
272
+ * the design be ignored?!"*). R1 now carries the exemption with
273
+ * this reason — see `docstoc-rail-tracking-and-snap-prop`. */}
274
+ <span className="kol-toc-label kol-helper-12 text-meta">{label}</span>
238
275
  <span aria-hidden="true" className="kol-toc-rule" />
239
276
  </a>
240
277
  )