@joewinke/jatui 0.22.0 → 0.24.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.
Files changed (71) hide show
  1. package/CLAUDE.md +72 -2
  2. package/dist/actions/reveal.d.ts +37 -0
  3. package/dist/actions/reveal.js +42 -0
  4. package/dist/components/AudioRecorder.svelte +529 -0
  5. package/dist/components/AudioRecorder.svelte.d.ts +30 -0
  6. package/dist/components/BulkActionBar.svelte +40 -0
  7. package/dist/components/BulkActionBar.svelte.d.ts +9 -0
  8. package/dist/components/ConfirmDialog.svelte +39 -35
  9. package/dist/components/CopyButton.svelte +5 -2
  10. package/dist/components/FloatingActionBar.svelte +55 -1
  11. package/dist/components/FloatingActionBar.svelte.d.ts +8 -1
  12. package/dist/components/IconSelector.svelte +417 -0
  13. package/dist/components/IconSelector.svelte.d.ts +55 -0
  14. package/dist/components/InfiniteGrid.svelte +305 -0
  15. package/dist/components/InfiniteGrid.svelte.d.ts +71 -0
  16. package/dist/components/InfiniteSearchGrid.svelte +131 -0
  17. package/dist/components/InfiniteSearchGrid.svelte.d.ts +67 -0
  18. package/dist/components/InputDialog.svelte +49 -38
  19. package/dist/components/KeyboardShortcutsOverlay.svelte +94 -3
  20. package/dist/components/KeyboardShortcutsOverlay.svelte.d.ts +61 -16
  21. package/dist/components/MediaCaptureIcon.svelte +23 -0
  22. package/dist/components/MediaCaptureIcon.svelte.d.ts +1 -1
  23. package/dist/components/ThumbnailGrid.svelte +137 -0
  24. package/dist/components/ThumbnailGrid.svelte.d.ts +42 -0
  25. package/dist/components/batch/BatchJobTable.svelte +549 -0
  26. package/dist/components/batch/BatchJobTable.svelte.d.ts +114 -0
  27. package/dist/components/batch/QuotaMeter.svelte +78 -0
  28. package/dist/components/batch/QuotaMeter.svelte.d.ts +16 -0
  29. package/dist/components/batch/batchQueue.d.ts +56 -0
  30. package/dist/components/batch/batchQueue.js +81 -0
  31. package/dist/components/batch/elapsedEstimate.d.ts +24 -0
  32. package/dist/components/batch/elapsedEstimate.js +25 -0
  33. package/dist/components/batch/pollOperation.d.ts +45 -0
  34. package/dist/components/batch/pollOperation.js +80 -0
  35. package/dist/components/batch/useElapsedEstimate.d.ts +10 -0
  36. package/dist/components/batch/useElapsedEstimate.js +38 -0
  37. package/dist/components/bulkActionBarKeys.d.ts +40 -0
  38. package/dist/components/bulkActionBarKeys.js +38 -0
  39. package/dist/components/infiniteGridLoader.d.ts +70 -0
  40. package/dist/components/infiniteGridLoader.js +141 -0
  41. package/dist/components/linked-columns/LinkedColumns.svelte +10 -22
  42. package/dist/components/linked-columns/LinkedColumns.svelte.d.ts +1 -9
  43. package/dist/components/linked-columns/types.d.ts +9 -0
  44. package/dist/components/linked-columns/types.js +1 -0
  45. package/dist/components/replay/ChapterTimeline.svelte +43 -25
  46. package/dist/components/replay/ChapterTimeline.svelte.d.ts +26 -1
  47. package/dist/components/table/columnResize.d.ts +38 -0
  48. package/dist/components/table/columnResize.js +117 -0
  49. package/dist/components/table/index.d.ts +2 -0
  50. package/dist/components/table/index.js +1 -0
  51. package/dist/components/toast/ToastItem.svelte +47 -2
  52. package/dist/components/variant/VariantMatrixPicker.svelte +181 -0
  53. package/dist/components/variant/VariantMatrixPicker.svelte.d.ts +37 -0
  54. package/dist/components/variant/variantCombinations.d.ts +75 -0
  55. package/dist/components/variant/variantCombinations.js +100 -0
  56. package/dist/index.d.ts +34 -3
  57. package/dist/index.js +30 -1
  58. package/dist/stores/toast.d.ts +8 -0
  59. package/dist/stores/toast.js +2 -0
  60. package/dist/styles/motion.css +100 -0
  61. package/dist/utils/audioRecorder.d.ts +28 -0
  62. package/dist/utils/audioRecorder.js +58 -0
  63. package/dist/utils/audioRecorderCore.d.ts +132 -0
  64. package/dist/utils/audioRecorderCore.js +447 -0
  65. package/dist/utils/clipboard.d.ts +25 -0
  66. package/dist/utils/clipboard.js +59 -0
  67. package/dist/utils/dateFormatters.d.ts +11 -2
  68. package/dist/utils/dateFormatters.js +22 -2
  69. package/dist/utils/keyboardShortcutRegistry.d.ts +152 -0
  70. package/dist/utils/keyboardShortcutRegistry.js +365 -0
  71. package/package.json +1 -1
package/CLAUDE.md CHANGED
@@ -33,6 +33,7 @@ Then import:
33
33
  | `VariablePicker` | Browse/insert template merge vars at caret | `groups: TemplateVarGroup[]`, `targetEl`, `onInsert` |
34
34
  | `CharacterCounter` | Shows chars remaining | `current: number`, `max: number` |
35
35
  | `ColorSelector` | Color picker from palette | `value` (bindable), `colors: string[]` |
36
+ | `IconSelector` | Searchable, paged icon picker — icon set is fully prop-driven (any component accepting `{ size, class }`, e.g. lucide-svelte), so no icon-library dependency; optional `icon` snippet for custom rendering. Keyboard: arrows move, Enter selects, Home/End, PgUp/PgDn page, Esc closes. | `icons: IconSelectorOption[]`, `selectedIcon` (bindable), `onSelect`, `label`, `colorClass`, `compact`, `columns`, `perPage`, `icon` |
36
37
  | `ImageUpload` | Drag-drop image upload | `value` (bindable), `accept`, `maxSize` |
37
38
  | `SmartImageEditor` | AI-powered image upload with background removal and multi-variant processing pipeline. **Preferred over `ImageUpload` in editor contexts.** Two modes: simple (drop-in replacement, outputs blob URL) and editor (POST multipart to backend → variant grid). | `value` (bindable), `spec?: ImageProcessingSpec`, `uploadEndpoint?`, `onComplete?`, `accept?`, `maxSizeMb?` |
38
39
  | `FileDropzone` | Multi-file/folder/ZIP drag-drop upload zone. Handles FileSystem API folder recursion, MIME/size validation, and shows a pending-file grid. Calls `onFilesSelected(files, errors)` with validated `File[]`. Accepts already-uploaded files via `uploadedFiles: FileDropzoneFile[]` for post-upload display. | `accept?: string[]`, `maxSize?`, `maxFiles?`, `multiple?`, `uploadedFiles?`, `isUploading?`, `isExtracting?`, `dropLabel?`, `dropSubtext?`, `onFilesSelected?` |
@@ -70,12 +71,16 @@ Then import:
70
71
  | `Tooltip` | Component-shaped wrapper around the shared `use:tooltip` action — for cases where you can't easily add an attribute to the target element (wrap a chip / badge / icon in the tooltip instead). Warm/cool window + `aria-describedby` are shared with the action. Prefer `use:tooltip` directly when you own the element. | `content: string`, `placement?`, `delayMs?`, `disabled?`, `display?`, `children` (snippet) |
71
72
  | `PresenceStack` | "Who's here" avatar stack for live-collaboration UI — max `maxVisible` avatars + "+N more" overflow bubble, tooltip per avatar ("X is editing FIELD" / "X is viewing"). Purely presentational — feed it viewers from any presence source (e.g. an SSE-backed presence composable). | `viewers: PresenceViewer[]`, `maxVisible?` (default 5), `size?`, `class?` |
72
73
  | `DirtyFormHost` | Composable form dirty-state manager — renders your form body plus a floating "Unsaved changes" pill + Cancel button; wires `beforeunload` and (optional) SvelteKit `beforeNavigate`. Callers who want bespoke pill/Cancel chrome use `createDirtyGuard()` directly and skip the component. Extracted from the F12 pattern in `/admin/roles/[id]`. | `baseline: T`, `current: T`, `onCancel?`, `beforeNavigate?`, `discardPrompt?`, `equals?`, `hideChrome?`, `children` (snippet with `{dirty, cancel}`) |
73
- | `BulkActionBar` | Floating action bar that appears when a `listNav` selection is non-empty. Simpler API than `FloatingActionBar`: takes a `Set`, `Array`, or plain number for `selection`; a flat `actions[]` with `{label, onSelect, destructive?}`; and now `groups[]` for dropdown-menu actions (each group is one button on the bar that opens a menu of `{label, onSelect}` items — same shape as `BulkAction`). Flat + grouped can be mixed. Reach for `FloatingActionBar` directly only when you need finer per-item dropdown styling (dot colors, icon snippets). | `selection: Set \| Array \| number`, `actions?: BulkAction[]`, `groups?: BulkActionGroup[]`, `onClear?`, `loading?`, `countLabel?` |
74
+ | `BulkActionBar` | Floating action bar that appears when a `listNav` selection is non-empty. Simpler API than `FloatingActionBar`: takes a `Set`, `Array`, or plain number for `selection`; a flat `actions[]` with `{label, onSelect, destructive?}`; and now `groups[]` for dropdown-menu actions (each group is one button on the bar that opens a menu of `{label, onSelect}` items — same shape as `BulkAction`). Flat + grouped can be mixed. Flat actions take a one-char `key` — the component registers the keydown handler itself (fired only while the bar is visible/idle; ignored while typing via `isTypingTarget`, and for modifier combos), and the key renders as a `<kbd>` hint on the button. A `loadingMessage` replaces the buttons with a spinner + message row while a bulk action is in flight (matches the IDE's fork). Reach for `FloatingActionBar` directly only when you need finer per-item dropdown styling (dot colors, icon snippets). | `selection: Set \| Array \| number`, `actions?: BulkAction[]`, `groups?: BulkActionGroup[]`, `onClear?`, `loading?`, `loadingMessage?`, `countLabel?` |
75
+ | `BatchJobTable` | Generic queue runner (harvested from vid-gen's BatchGenerator) — `<BatchJobTable generics T>` over bindable `JobRow<T>[]`; host supplies async `run(data, {signal, attempt, reportProgress})`. Concurrency cap, optional per-run `processLimit` (quota budget), rolling-average ETA, per-row status badge / progress / elapsed / retry / skip / cancel / reorder — each cell a snippet override (`statusCell`, `progressCell`, `elapsedCell`, `actionsCell`), host data columns via `headers` + `cells`, extra toolbar content via `toolbar`. Uses `showToast` / `showConfirm` (mount `ToastHost` + `ConfirmDialog`). Exposes `start()`/`pause()`/`cancel()`/`retryFailed()`/`clearFinished()`. Pure scheduling helpers exported too (`nextJobsToStart`, `moveJob`, `summarizeJobs`, `toJobRows`). | `jobs` (bindable), `run`, `concurrencyLimit?` (3), `processLimit?`, `autoStart?`, `notify?`, `confirmDestructive?`, `onJobDone?`, `onJobFailed?`, `onBatchDone?` |
76
+ | `QuotaMeter` | "312 / 500 credits · resets in 4h 10m" bar — warning tint at `warnAt`, error at 100%. Presentational; the host owns the ledger. | `used`, `limit`, `resetsAt?`, `label?`, `unit?`, `warnAt?` (0.8), `size?` |
77
+ | `VariantMatrixPicker` | Category/option chip grid for building a combinatorial variant matrix (harvested from vid-gen's FrameVariantGenerator, presets stripped — caller-supplied `categories`). Each category tab shows a live selection count and an additive/multiplicative badge: multiplicative selections cartesian-multiply the total, additive ones stack flat onto every combination. Live combination count in the footer; algebra exported separately as pure functions (`generateCombinations`, `countCombinations`, `variantCategoryMode`, `findVariantOption`, `generateVariantFilename`). | `categories: VariantCategory[]`, `selected` (bindable `VariantSelection`), `forceMode?`, `showCount?` (true), `countLabel?`, `onChange?`, `class?` |
74
78
 
75
79
  ### Media
76
80
 
77
81
  | Component | Purpose | Key Props |
78
82
  |-----------|---------|-----------|
83
+ | `ThumbnailGrid` | Generic thumbnail strip (chimaro port) — image tiles with text-tile fallback, +N overflow counter, remove/add affordances. Decoupled from any media library: pass `{id, url, alt}` items, or a `cell` snippet for full control of tile contents. | `items: ThumbnailItem[]`, `max`, `size` (`xs\|sm\|md\|lg`), `removable`+`onRemove`, `addable`+`onAdd`, `onSelect`, `cell` snippet |
79
84
  | `MediaWorkbench` | Full-featured image editor modal — upload, AI generate, AI edit, background removal, annotation canvas. **Preferred over `SmartImageEditor` for new image fields.** Two-column modal: preview left, 4 action tabs right (Generate, Edit, Remove BG, Annotate). Save uploads blob to `uploadEndpoint` or emits `onComplete`. Requires proxy endpoints from jst-23w1l.2. | `value` (bindable `string \| null`), `uploadEndpoint?` (default `/api/media`), `onComplete?`, `accept?` |
80
85
  | `MediaLightbox` | Full-screen media preview — z-9040 backdrop, arrow-key paging when given an array, `+/-/0` zoom, download action, Escape/backdrop close. **Use this for the general "just show it big" case** — replaces hand-rolled `fixed inset-0 z-[9040]` modals. For annotated / markup-overlaid image previews, use `ImageLightbox` instead. | `src?`, `filename?`, `kind?: 'image' \| 'video'`, `downloadUrl?`, `items?: MediaItem[]`, `initialIndex?`, `onClose?`, `zIndex?` |
81
86
  | `VideoPlayer` | **The default video surface — reach for this over a bare `<video controls>`.** One timeline band carrying a waveform-envelope scrubber, chapter flags (top edge), bookmark diamonds (bottom edge), diarized speaker colouring and scroll-to-zoom, plus an optional transcript pane (click to seek, active line auto-scroll, solo/skip per speaker, `T` toggle) and talk-time legend. **Every intelligence prop is optional** — a bare mp4 still gets the scrubber, hover-time card, wheel volume and the full keyboard map (Space/K, arrows/J/L, vol/mute, −/= speed + Backspace reset, </> frame step, [/] chapters, B bookmark, ? help overlay), and each pane lights up as its data arrives (nothing shifts layout when diarization lands late). Wheel means volume over the video (shift = seek, alt = speed) and zoom over the band. Internals live in `src/lib/video/` (band/keys/rate/deepLink/settings + contracts for audio/night/presence — see that README; gap audit vs the extension: `VIDEO-PLAYER-GAP-AUDIT.md`). | `src`, `chapters?`, `speakers?`, `transcript?`, `bookmarks?`, `waveform?`, `talkTime?`, `startAt?`, `poster?`, `captionsSrc?`, `crossorigin?` (`'anonymous'` — what frame-reading features need), `syncUrlTime?` (`?t=` deep-link, opt-in), `transcriptPosition?` (`'below' \| 'side'`), `onTimeUpdate?`, `onAddBookmark?`, `onBookmarkContext?` |
@@ -134,6 +139,8 @@ import { loadLiveKit, isLiveKitAvailable, LiveKitNotInstalledError } from 'jatui
134
139
  |-----------|---------|-----------|
135
140
  | `ResizablePanel` | Resizable container | `minWidth`, `maxWidth`, `defaultWidth` |
136
141
  | `ResizableDivider` | Drag handle between panels | `onResize` |
142
+ | `InfiniteGrid` | Cursor-paged infinite-scroll grid (from chimaro). `loadMore(cursor, {signal})` resolves `{items, nextCursor, total?}` (`nextCursor: null` = end); cells render through the `renderItem(item, i)` snippet. Change `resetKey` to start over — the in-flight page is aborted and a late page for the old key is dropped. One request in flight; items de-duped by `getKey`; short pages auto-chain until the sentinel leaves the screen; distinct skeleton / empty / exhausted / error(+Retry) states; non-item rows are `overflow-anchor: none` so scroll anchoring can't runaway-load. Scroll root = `root` or nearest scrollable ancestor. Exposes `reset()`/`retry()`. Not virtualized. Pure core: `createInfiniteLoader`. | `loadMore`, `renderItem` (snippet), `getKey?`, `resetKey?`, `columns?` / `minItemWidth?`, `gap?`, `root?`, `loadAhead?` (400px), `skeleton?`/`empty?`/`end?`/`error?` (snippets), `emptyText?`, `endText?`, `onStateChange?` |
143
+ | `InfiniteSearchGrid` | Search box + `InfiniteGrid`. Query is debounced, then becomes the grid's `resetKey` (Enter commits now, Escape clears). Below `minQueryLength` no request is made and `prompt` shows. All other `InfiniteGrid` props pass through. | `search(query, cursor, {signal})`, `renderItem` (snippet), `query` (bindable), `debounceMs?` (300), `minQueryLength?` (0), `placeholder?`, `noResultsText?`/`noResults?`, `prompt?`/`promptText?`, `toolbar?` (snippet) |
137
144
  | `SectionNav` | Sticky in-page section index for long single-column pages — vertical rail beside the content on xl+, wrapping chip row above it below xl. Built-in scroll-spy (omit `activeId` and it just works; `bind:activeId` to observe/control — the spy writes back to it): on every observer callback + rAF-throttled scroll/resize it measures ALL sections and activates the last one whose top is at/above the activation line (`activationRatio`, default 0.3 of the scroller height); at the bottom of the page the last section wins. Clicking an item activates it immediately and holds the spy off until the smooth scroll lands (`scrollend`, or 800ms after the last scroll event); reduced-motion scrolls instantly. Pure decision fn exported as `computeActiveSection(rects, viewport, scroll, ratio?)`. Amber dot per `dirtyIds` marks sections holding unsaved edits. `go()` scrolls AND moves focus so keyboard/SR users land where the click did. j/k/Enter via `listNav`. Layout contract: parent is a `flex flex-wrap gap-8 items-start` row, content column `flex-1 min-w-0`; sections need `id` + `scroll-mt-*`. | `sections: {id,label}[]`, `activeId?` (bindable), `dirtyIds?: Set<string>`, `ariaLabel?`, `spy?`, `activationRatio?` (0.3) |
138
145
 
139
146
  ### Audio & Voice
@@ -143,6 +150,29 @@ import { loadLiveKit, isLiveKitAvailable, LiveKitNotInstalledError } from 'jatui
143
150
  | `AudioWaveform` | Audio waveform visualizer | `audioUrl`, `isPlaying` |
144
151
  | `VoicePlayer` | Audio playback with controls | `src`, `autoplay` |
145
152
  | `SpeechForm` | Voice-to-text form input | `onTranscript` |
153
+ | `AudioRecorder` | Voice-memo recorder UI (small pill / large panel with canvas waveform) — markup over the headless factory below | `variant`, `maxDurationMs`, `onRecordStart/Pause/Resume/Stop/Cancel`, `onError` |
154
+
155
+ ```typescript
156
+ // Headless audio capture (jatui-is2ow.8) — the factory behind AudioRecorder.
157
+ // Permission requests, MIME negotiation, record/pause/resume/stop/cancel state
158
+ // machine, pause-aware wall-clock timing, AnalyserNode level meter, blob out.
159
+ // NO transcription — hosts keep their own pipeline and consume onStop's blob
160
+ // (the jat IDE's VoiceMicButton/VoiceInput swap uses it this way).
161
+ import { createAudioRecorder } from "jatui"
162
+ const rec = createAudioRecorder({
163
+ mode: "push", // 'toggle' (default) | 'push' (hold-to-talk: pointerdown→start, pointerup→stop;
164
+ // a stop during stream acquisition cancels silently, matching VoiceMicButton)
165
+ maxDurationMs: 0, // auto-stop ceiling; 0 = until stopped
166
+ onStart?, onPause?, onResume?, onCancel?,
167
+ onStop?, // (result: AudioRecordingResult) — blob/mimeType/durationSeconds/sizeBytes;
168
+ // may be async, recorder stays 'processing' until it settles
169
+ onError?, // (err: { kind, message }) — kind: unsupported|denied|no-mic|start-failed|too-short|handler-failed
170
+ onFrame?, // ({ level, frequency, time }) per animation frame — draw your own visualization
171
+ })
172
+ // Reactive state (Svelte 5 getters): rec.phase ('idle'|'starting'|'recording'|'processing'|'done'),
173
+ // rec.paused, rec.level (0–100), rec.elapsedMs, rec.sizeBytes, rec.error, rec.supported
174
+ // Methods: start() stop() pause() resume() cancel() reset() destroy()
175
+ ```
146
176
 
147
177
  ### PDF
148
178
 
@@ -195,7 +225,26 @@ const callbacks: AnnotationCallbacks = {
195
225
 
196
226
  | Component | Purpose | Key Props |
197
227
  |-----------|---------|-----------|
198
- | `KeyboardShortcutsOverlay` | `?`-triggered modal listing keyboard shortcuts. Ignores typing targets and Ctrl/Meta/Alt combos. Supports a flat `shortcuts` list or grouped `sections` with optional per-key color tinting. | `shortcuts?: KeyboardShortcut[]`, `sections?: KeyboardShortcutSection[]`, `title?`, `open` (bindable) |
228
+ | `KeyboardShortcutsOverlay` | `?`-triggered modal listing keyboard shortcuts. Ignores typing targets and Ctrl/Meta/Alt combos. Supports a flat `shortcuts` list, grouped `sections` with optional per-key color tinting, or a shortcut `registry` (lists what can fire right now, and pushes a modal context while open so page shortcuts don't fire behind it). App-specific sections (marks, list motions, cockpit panels) come in via `extraSections` — rendered after the primary content, before the meta rows, in every mode — plus an `extra` snippet for custom footer content. Precedence: `sections` > `registry` > `shortcuts`; `extraSections`/`extra` always append. | `shortcuts?: KeyboardShortcut[]`, `sections?: KeyboardShortcutSection[]`, `registry?: ShortcutRegistry`, `extraSections?: KeyboardShortcutSection[]`, `extra?: Snippet`, `title?`, `open` (bindable) |
229
+
230
+ **Shortcut registry** (`createShortcutRegistry()`, or the lazy `getShortcutRegistry()` singleton) — the data source for the overlay, so apps stop hand-wiring a keydown handler plus a separate list for the overlay. Shortcuts register against a **context** and return an unregister fn (return it from `$effect`). Contexts form a stack: `global` is the bottom, `pushContext(name, { modal?, label? })` returns a pop fn, the top context shadows the same combo lower down, and a `modal` context suppresses everything beneath it (including `global`). Handled for you: no firing while typing in text inputs/textarea/select/contenteditable (opt in per shortcut with `allowInInput`), IME composition and `defaultPrevented` events are skipped, `mod` = ⌘ on macOS / Ctrl elsewhere, exact modifier matching (`?` doesn't need `shift`), and macOS Option combos fall back to `event.code`. Per shortcut: `enabled()`, `allowRepeat`, `preventDefault`, `group` (overlay section), `hidden`, `label` (keycap override).
231
+
232
+ ```svelte
233
+ <script>
234
+ import { createShortcutRegistry, KeyboardShortcutsOverlay, SIDEBAR_TOGGLE_KEY } from "jatui"
235
+ const shortcuts = createShortcutRegistry({ contextLabels: { list: "Task list" } })
236
+ $effect(() => shortcuts.attach()) // keydown on window
237
+ $effect(() => shortcuts.pushContext("list"))
238
+ $effect(() => shortcuts.register([
239
+ { key: "j", description: "Next", context: "list", action: next },
240
+ { key: "mod+k", description: "Search", action: openPalette, allowInInput: true },
241
+ { key: SIDEBAR_TOGGLE_KEY, description: "Toggle sidebar", action: toggleSidebar },
242
+ ]))
243
+ </script>
244
+ <KeyboardShortcutsOverlay registry={shortcuts} />
245
+ ```
246
+
247
+ **Sidebar convention: `SIDEBAR_TOGGLE_KEY` = `mod+\`** (Ctrl+\ / ⌘\). Do NOT use Ctrl+B: Chrome reserves it (bookmarks bar). Also avoid Ctrl+T/W/N, which the browser keeps. The overlay owns `?`, so don't register it. Helpers: `formatShortcut(def | "mod+k", platform?)` → `⌘K` / `Ctrl+K`, `isTypingTarget(el)`, `parseShortcutCombo`, `detectShortcutPlatform`.
199
248
 
200
249
  ### Maps (Google Maps)
201
250
 
@@ -258,6 +307,20 @@ import { qrMatrix, qrCanvas } from "jatui" // deep-li
258
307
  import { createHistory, pushState, undo, redo, beginChange, endChange, untracked, clearHistory, MAX_UNDO_DEPTH } from "jatui"
259
308
  import type { History } from "jatui"
260
309
 
310
+ // Batch/queue primitives (jatui-m4uz3.7) — companions to BatchJobTable.
311
+ import { useElapsedEstimate } from "jatui" // rune: eta.record(ms); eta.remainingMs(queued, runningElapsed, concurrency)
312
+ import { pollOperation, PollTimeoutError } from "jatui"
313
+ // await pollOperation(async ({ signal, attempt }) => op.done ? { done: true, value } : { done: false, progress },
314
+ // { intervalMs, timeoutMs, backoff, signal, onProgress }) — rejects on timeout / abort / thrown check
315
+ import { formatElapsed, rollingAverage, estimateRemainingMs } from "jatui"
316
+
317
+ // Variant combination algebra (jatui-m4uz3.8) — pure functions behind VariantMatrixPicker.
318
+ import { generateCombinations, countCombinations, type VariantCategory, type VariantSelection } from "jatui"
319
+ // VariantCategory — { id, label, icon?, mode?: 'multiplicative' | 'additive', options: VariantOption[] }
320
+ // VariantOption — { id, label, prompt? }
321
+ // VariantSelection — { [categoryId]: string[] } (selected option ids per category)
322
+ // generateCombinations(categories, selected, { forceMode? }) → [{ modifierIds, prompt, options }]
323
+
261
324
  // Date formatting
262
325
  import { formatRelativeTime, formatDate, formatShortDate, formatDateTime } from "jatui"
263
326
 
@@ -275,6 +338,13 @@ import { statusColor, priorityColor, typeIcon, statusLabel } from "jatui"
275
338
  // Phone formatting
276
339
  import { formatPhoneNumber, isValidPhoneNumber } from "jatui"
277
340
 
341
+ // Clipboard — secure-context aware: falls back to hidden-textarea +
342
+ // document.execCommand('copy') on plain-HTTP origins where navigator.clipboard
343
+ // is undefined (fixes "copy button silently does nothing" over Tailscale/LAN).
344
+ // copyText resolves false when both paths fail; readClipboardText resolves
345
+ // null when the read API is missing or denied.
346
+ import { copyText, readClipboardText } from "jatui"
347
+
278
348
  // Currency
279
349
  import { formatCurrency } from "jatui"
280
350
 
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Scroll-reveal action — reveals an element the first time it scrolls into
3
+ * the viewport (IntersectionObserver, fires once, then unobserves).
4
+ *
5
+ * Ported from chimaro's `use:reveal` (jatui-m4uz3.3). The public API is kept
6
+ * chimaro-compatible (animation / delay / threshold) so chimaro's local copy
7
+ * is deletable in favor of this import — but the animista.css dependency is
8
+ * NOT shipped: each named variant remaps onto a `jatui-reveal-*` class in
9
+ * `jatui/styles/motion.css`, whose keyframes and timing are built entirely
10
+ * from the `--motion-*` tokens.
11
+ *
12
+ * Usage:
13
+ * <div use:reveal> → fade-in-bottom on scroll
14
+ * <div use:reveal={{ animation: 'scale-in-center' }}>
15
+ * <div use:reveal={{ delay: 0.2 }}> → 200ms staggered delay
16
+ * <div use:reveal={{ threshold: 0.3 }}> → triggers at 30% visibility
17
+ *
18
+ * Note: `delay` is in SECONDS (chimaro's contract — CSS animation-delay
19
+ * units), unlike staggerEntrance's ms-based options. Use fractional values
20
+ * for staggers (0.2 = 200ms).
21
+ */
22
+ export type RevealVariant = 'fade-in' | 'fade-in-bottom' | 'fade-in-top' | 'fade-in-left' | 'fade-in-right' | 'scale-in-center';
23
+ export interface RevealOptions {
24
+ /** Named entrance variant, rendered by a `jatui-reveal-*` class in motion.css. Default 'fade-in-bottom'. */
25
+ animation?: RevealVariant;
26
+ /** Delay in SECONDS before the entrance runs (fill mode holds the from-state during the wait). Default 0. */
27
+ delay?: number;
28
+ /** Fraction of the element that must be visible before revealing (0–1). Default 0.1. */
29
+ threshold?: number;
30
+ }
31
+ /** Class applied while the element is waiting to be revealed (opacity: 0). */
32
+ export declare const REVEAL_HIDDEN_CLASS = "jatui-reveal-hidden";
33
+ /** Base class carrying the token-driven duration/easing/fill. */
34
+ export declare const REVEAL_BASE_CLASS = "jatui-reveal";
35
+ export declare function reveal(node: HTMLElement, options?: RevealOptions): {
36
+ destroy(): void;
37
+ };
@@ -0,0 +1,42 @@
1
+ /** Class applied while the element is waiting to be revealed (opacity: 0). */
2
+ export const REVEAL_HIDDEN_CLASS = 'jatui-reveal-hidden';
3
+ /** Base class carrying the token-driven duration/easing/fill. */
4
+ export const REVEAL_BASE_CLASS = 'jatui-reveal';
5
+ function variantClass(animation) {
6
+ return `${REVEAL_BASE_CLASS}-${animation}`;
7
+ }
8
+ export function reveal(node, options) {
9
+ const animation = options?.animation ?? 'fade-in-bottom';
10
+ const threshold = options?.threshold ?? 0.1;
11
+ // Respect prefers-reduced-motion: skip the animation entirely, show content.
12
+ const prefersReducedMotion = typeof window !== 'undefined' &&
13
+ window.matchMedia?.('(prefers-reduced-motion: reduce)').matches;
14
+ // No window (SSR) or no IntersectionObserver: stay visible, do nothing.
15
+ const canObserve = !prefersReducedMotion &&
16
+ typeof window !== 'undefined' &&
17
+ typeof IntersectionObserver !== 'undefined';
18
+ if (!canObserve) {
19
+ return { destroy() { } };
20
+ }
21
+ node.classList.add(REVEAL_HIDDEN_CLASS);
22
+ if (options?.delay) {
23
+ node.style.animationDelay = `${options.delay}s`;
24
+ }
25
+ const observer = new IntersectionObserver((entries) => {
26
+ for (const entry of entries) {
27
+ if (entry.isIntersecting) {
28
+ node.classList.remove(REVEAL_HIDDEN_CLASS);
29
+ node.classList.add(REVEAL_BASE_CLASS, variantClass(animation));
30
+ observer.unobserve(node);
31
+ }
32
+ }
33
+ }, { threshold });
34
+ observer.observe(node);
35
+ return {
36
+ destroy() {
37
+ observer.disconnect();
38
+ node.classList.remove(REVEAL_HIDDEN_CLASS, REVEAL_BASE_CLASS, variantClass(animation));
39
+ node.style.animationDelay = '';
40
+ },
41
+ };
42
+ }