wiki-formant 0.30.0 → 0.31.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/README.md CHANGED
@@ -258,6 +258,13 @@ const { query, setQuery, items, highlight, setHighlight, onKeyDown, reset } =
258
258
 
259
259
  `useLinkPreview` is the Wikipedia-style hover card: one delegated listener rather than a component per link, which is what makes it viable over an article holding hundreds of anchors. Two wikis had written the same ninety lines — same intent delay, same grace period for the cursor to cross into the card, same clamp arithmetic, same cache. They differed in three things, and those three are the options: which anchors are eligible, what fetches a preview, and how tall the card is.
260
260
 
261
+ `useSnapRow` is a carousel as a row scrolled sideways under `scroll-snap-type: x mandatory`, so a finger swipes it with no script; AcuiQ's protocol card, which swapped one point for the next in state, could not be swiped at all. The hook is the rest: `step` for arrows and arrow keys, wrapping round at either end, `go(i)` for a dot or for opening a lightbox on the picture clicked, and `at` for the counter. It ships no markup and no class names, since miow's rows hold pictures and AcuiQ's hold whole acupoints.
262
+
263
+ ```tsx
264
+ const { ref, onScroll, at, step, go } = useSnapRow();
265
+ <div ref={ref} onScroll={onScroll} className="row">{items}</div>
266
+ ```
267
+
261
268
  `useClickOutside` is fifteen lines and was in all three. Only one had the `offsetParent` check, and without it a container hidden at the current breakpoint still answers outside-clicks — so on a phone, a tap anywhere dismisses the popover the reader is looking at, because the hidden desktop copy got there first.
262
269
 
263
270
  ### The listbox contract
package/dist/react.d.ts CHANGED
@@ -349,6 +349,31 @@ export declare function useCopy<K extends string = string>(revertAfter?: number)
349
349
  copied: K | null;
350
350
  copy: (text: string, key?: K) => Promise<boolean>;
351
351
  };
352
+ export interface SnapRow<T extends HTMLElement> {
353
+ /** The row: give it `overflow-x: auto` and `scroll-snap-type: x mandatory`, and its children `scroll-snap-align: start`. */
354
+ ref: RefObject<T | null>;
355
+ /** The row's `onScroll`, which keeps `at` current. */
356
+ onScroll: () => void;
357
+ /** The item in view, or the last once the row is scrolled to its end: for a counter or dots. */
358
+ at: number;
359
+ /** Most of a view along, as an arrow or an arrow key moves it, wrapping round from either end. */
360
+ step: (dir: 1 | -1) => void;
361
+ /** Item `i` to the start of the row, as a dot moves it; `smooth: false` to open on it. */
362
+ go: (i: number, smooth?: boolean) => void;
363
+ }
364
+ /**
365
+ * A carousel as a row scrolled sideways and snapping into place. A finger
366
+ * swipes it with no script at all, which a carousel that swaps one item for
367
+ * the next in state cannot do: AcuiQ's protocol card moved only by its
368
+ * buttons. This is the rest, with no markup and no class names, since what the
369
+ * row holds differs from one use to the next.
370
+ *
371
+ * `step` scrolls by most of a view and lets the snap settle it, so a row of
372
+ * items narrower than itself pages a screenful at a time and a row of full-width
373
+ * ones moves one. It wraps at either end, because an arrow that does nothing
374
+ * there reads as broken. Motion honours `prefers-reduced-motion`.
375
+ */
376
+ export declare function useSnapRow<T extends HTMLElement = HTMLDivElement>(): SnapRow<T>;
352
377
  /**
353
378
  * The one thing hooks cannot express, so it is worth owning once.
354
379
  *
package/dist/react.js CHANGED
@@ -698,6 +698,52 @@ export function useCopy(revertAfter = 2000) {
698
698
  }, [revertAfter]);
699
699
  return { copied, copy };
700
700
  }
701
+ /**
702
+ * A carousel as a row scrolled sideways and snapping into place. A finger
703
+ * swipes it with no script at all, which a carousel that swaps one item for
704
+ * the next in state cannot do: AcuiQ's protocol card moved only by its
705
+ * buttons. This is the rest, with no markup and no class names, since what the
706
+ * row holds differs from one use to the next.
707
+ *
708
+ * `step` scrolls by most of a view and lets the snap settle it, so a row of
709
+ * items narrower than itself pages a screenful at a time and a row of full-width
710
+ * ones moves one. It wraps at either end, because an arrow that does nothing
711
+ * there reads as broken. Motion honours `prefers-reduced-motion`.
712
+ */
713
+ export function useSnapRow() {
714
+ const ref = useRef(null);
715
+ const [at, setAt] = useState(0);
716
+ const onScroll = useCallback(() => {
717
+ const row = ref.current;
718
+ if (!row)
719
+ return;
720
+ const items = [...row.children];
721
+ const left = row.getBoundingClientRect().left;
722
+ const offsets = items.map(el => Math.abs(el.getBoundingClientRect().left - left));
723
+ setAt(row.scrollLeft >= snapEnd(row) - 1 ? items.length - 1 : offsets.indexOf(Math.min(...offsets)));
724
+ }, []);
725
+ const go = useCallback((i, smooth = true) => {
726
+ const row = ref.current;
727
+ const n = row?.children.length ?? 0;
728
+ const el = row?.children[((i % n) + n) % n];
729
+ if (row && el)
730
+ snapTo(row, row.scrollLeft + el.getBoundingClientRect().left - row.getBoundingClientRect().left, smooth);
731
+ }, []);
732
+ const step = useCallback((dir) => {
733
+ const row = ref.current;
734
+ if (!row)
735
+ return;
736
+ if (dir > 0 && row.scrollLeft >= snapEnd(row) - 1)
737
+ snapTo(row, 0);
738
+ else if (dir < 0 && row.scrollLeft <= 1)
739
+ snapTo(row, snapEnd(row));
740
+ else
741
+ snapTo(row, row.scrollLeft + dir * row.clientWidth * 0.8);
742
+ }, []);
743
+ return { ref, onScroll, at, step, go };
744
+ }
745
+ const snapEnd = (row) => row.scrollWidth - row.clientWidth;
746
+ const snapTo = (row, left, smooth = true) => row.scrollTo({ left, behavior: smooth && !matchMedia('(prefers-reduced-motion: reduce)').matches ? 'smooth' : 'auto' });
701
747
  // ---- error boundary ---------------------------------------------------------
702
748
  /**
703
749
  * The one thing hooks cannot express, so it is worth owning once.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wiki-formant",
3
- "version": "0.30.0",
3
+ "version": "0.31.0",
4
4
  "description": "The portable half of a wiki: derived taxonomy and facet controls, a version-negotiating MCP transport, markdown twins, block rendering, a rich-text editor engine, and conditional-GET plumbing.",
5
5
  "type": "module",
6
6
  "license": "MIT",