──────────────────────────────────────────────────────────────────
motion@0.2.1 — Parallax + TiltGallery
──────────────────────────────────────────────────────────────────
+ Parallax.tsx
- scroll-driven translate3d via rAF + window.scroll
- speed multiplier + axis (y / x) + reduced-motion bypass
- listener cleanup + cancelAnimationFrame on unmount
- WAVE 13.D.1
+ TiltGallery.tsx
- horizontally-scrolling rail of <TiltCard>-style tiles
- per-tile cursor-tracking rotateX/Y + glare gradient
- role=region + arrow-key scrolling (←/→) + scroll-snap
- reduced-motion strips tilt + glare, keeps the rail
- WAVE 13.D.5
+ 5 new tests (Parallax x 2, TiltGallery x 3) — 28/28 passing
+ index.ts: exports both + types
+ package.json: 0.2.0 → 0.2.1
──────────────────────────────────────────────────────────────────
ai-ui@0.6.0 — Wave 9.E composition surfaces
──────────────────────────────────────────────────────────────────
+ Markdown.tsx
- dep-free subset renderer: h1-h3 / **bold** / *italic* /
`code` / fenced code / ul + ol / [text](url)
- inline `[cite:<id>]` chips resolved from a citations
registry (missing ids render as [?] — failure mode is loud)
- WAVE 9.E.1
+ CodeDiff.tsx
- line-LCS diff in <100 LOC, zero deps
- split (2-col) and unified views; tinted add/del rows
- WAVE 9.E.2
+ ExplainThis.tsx
- listens for selectionchange, pops 'Explain' CTA over the
selection rect when inside the wrapper + ≥ minLength chars
- fires onExplain({ text, rect }) so hosts can open a richer
side panel if preferred
- WAVE 9.E.3
+ usePromptHistory.ts
- bash-style ↑/↓ recall with localStorage persistence
(storage key configurable; null = in-memory for tests/SSR)
- dedupes consecutive duplicates + trims to capacity
- WAVE 9.E.4
+ useTokenCount.ts
- cheap estimator (default ~4 chars/token; configurable for
code/CJK) + optional USD cost
- memoised — stable across re-renders
- WAVE 9.E.5
+ 19 new tests in src/__tests__/composition.test.tsx — 98/98 passing
+ index.ts: '0.6 surfaces' section exports all 5 + types
+ package.json: 0.5.0 → 0.6.0
Showcase routes + roadmap flips land in the paired showcase commit.
109 lines
3.4 KiB
TypeScript
109 lines
3.4 KiB
TypeScript
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
|
|
export interface UsePromptHistoryOptions {
|
|
/**
|
|
* Storage key — should be product-scoped (e.g. `notes:chat:history`).
|
|
* Pass `null` to disable persistence (in-memory only).
|
|
*/
|
|
storageKey?: string | null;
|
|
/** Maximum number of prompts retained. Default 50. */
|
|
capacity?: number;
|
|
}
|
|
|
|
export interface UsePromptHistoryHelpers {
|
|
/** The full history array, newest LAST. */
|
|
history: string[];
|
|
/** Append a prompt — dedupes consecutive duplicates + trims to capacity. */
|
|
push: (prompt: string) => void;
|
|
/** Clear all stored prompts. */
|
|
clear: () => void;
|
|
/** Cursor-up — returns the previous prompt (or null at the top). */
|
|
prev: () => string | null;
|
|
/** Cursor-down — returns the next prompt (or null at the bottom). */
|
|
next: () => string | null;
|
|
/** Reset the cursor — call when the user starts typing fresh. */
|
|
resetCursor: () => void;
|
|
}
|
|
|
|
/**
|
|
* `usePromptHistory` — bash-style ↑/↓ recall for prompt composers.
|
|
*
|
|
* Wave 9.E.4. Persistence is `localStorage` by default, scoped via
|
|
* `storageKey`. Pass `null` for in-memory only (useful for tests +
|
|
* SSR-prerender safety).
|
|
*/
|
|
export function usePromptHistory(
|
|
options: UsePromptHistoryOptions = {},
|
|
): UsePromptHistoryHelpers {
|
|
const { storageKey = 'bytelyst:prompt-history', capacity = 50 } = options;
|
|
|
|
// Lazy initial value — never touches `localStorage` during SSR.
|
|
const [history, setHistory] = useState<string[]>(() => {
|
|
if (!storageKey || typeof window === 'undefined') return [];
|
|
try {
|
|
const raw = window.localStorage.getItem(storageKey);
|
|
if (!raw) return [];
|
|
const parsed = JSON.parse(raw);
|
|
return Array.isArray(parsed) ? (parsed as string[]) : [];
|
|
} catch {
|
|
return [];
|
|
}
|
|
});
|
|
// Cursor: -1 means 'not navigating'. >=0 indexes from the END (so 0
|
|
// is the newest, history.length-1 is the oldest). This matches the
|
|
// bash ↑ recall convention.
|
|
const cursorRef = useRef(-1);
|
|
|
|
useEffect(() => {
|
|
if (!storageKey || typeof window === 'undefined') return;
|
|
try {
|
|
window.localStorage.setItem(storageKey, JSON.stringify(history));
|
|
} catch {
|
|
/* quota / disabled — ignore silently */
|
|
}
|
|
}, [storageKey, history]);
|
|
|
|
const push = useCallback(
|
|
(prompt: string) => {
|
|
const trimmed = prompt.trim();
|
|
if (!trimmed) return;
|
|
setHistory((cur) => {
|
|
// De-dupe consecutive duplicate.
|
|
if (cur[cur.length - 1] === trimmed) return cur;
|
|
const next = [...cur, trimmed];
|
|
return next.length > capacity ? next.slice(next.length - capacity) : next;
|
|
});
|
|
cursorRef.current = -1;
|
|
},
|
|
[capacity],
|
|
);
|
|
|
|
const clear = useCallback(() => {
|
|
setHistory([]);
|
|
cursorRef.current = -1;
|
|
}, []);
|
|
|
|
const prev = useCallback((): string | null => {
|
|
if (history.length === 0) return null;
|
|
const newIdx = Math.min(history.length - 1, cursorRef.current + 1);
|
|
cursorRef.current = newIdx;
|
|
return history[history.length - 1 - newIdx] ?? null;
|
|
}, [history]);
|
|
|
|
const next = useCallback((): string | null => {
|
|
if (history.length === 0) return null;
|
|
if (cursorRef.current <= 0) {
|
|
cursorRef.current = -1;
|
|
return null;
|
|
}
|
|
cursorRef.current -= 1;
|
|
return history[history.length - 1 - cursorRef.current] ?? null;
|
|
}, [history]);
|
|
|
|
const resetCursor = useCallback(() => {
|
|
cursorRef.current = -1;
|
|
}, []);
|
|
|
|
return { history, push, clear, prev, next, resetCursor };
|
|
}
|