@joewinke/jatui 0.22.0 → 0.23.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 (45) hide show
  1. package/CLAUDE.md +41 -1
  2. package/dist/actions/reveal.d.ts +37 -0
  3. package/dist/actions/reveal.js +42 -0
  4. package/dist/components/AudioRecorder.svelte +749 -0
  5. package/dist/components/AudioRecorder.svelte.d.ts +36 -0
  6. package/dist/components/IconSelector.svelte +417 -0
  7. package/dist/components/IconSelector.svelte.d.ts +55 -0
  8. package/dist/components/InfiniteGrid.svelte +305 -0
  9. package/dist/components/InfiniteGrid.svelte.d.ts +71 -0
  10. package/dist/components/InfiniteSearchGrid.svelte +131 -0
  11. package/dist/components/InfiniteSearchGrid.svelte.d.ts +67 -0
  12. package/dist/components/KeyboardShortcutsOverlay.svelte +40 -3
  13. package/dist/components/KeyboardShortcutsOverlay.svelte.d.ts +31 -16
  14. package/dist/components/MediaCaptureIcon.svelte +23 -0
  15. package/dist/components/MediaCaptureIcon.svelte.d.ts +1 -1
  16. package/dist/components/ThumbnailGrid.svelte +137 -0
  17. package/dist/components/ThumbnailGrid.svelte.d.ts +42 -0
  18. package/dist/components/batch/BatchJobTable.svelte +549 -0
  19. package/dist/components/batch/BatchJobTable.svelte.d.ts +114 -0
  20. package/dist/components/batch/QuotaMeter.svelte +78 -0
  21. package/dist/components/batch/QuotaMeter.svelte.d.ts +16 -0
  22. package/dist/components/batch/batchQueue.d.ts +56 -0
  23. package/dist/components/batch/batchQueue.js +81 -0
  24. package/dist/components/batch/elapsedEstimate.d.ts +24 -0
  25. package/dist/components/batch/elapsedEstimate.js +25 -0
  26. package/dist/components/batch/pollOperation.d.ts +45 -0
  27. package/dist/components/batch/pollOperation.js +80 -0
  28. package/dist/components/batch/useElapsedEstimate.d.ts +10 -0
  29. package/dist/components/batch/useElapsedEstimate.js +38 -0
  30. package/dist/components/infiniteGridLoader.d.ts +70 -0
  31. package/dist/components/infiniteGridLoader.js +141 -0
  32. package/dist/components/table/columnResize.d.ts +38 -0
  33. package/dist/components/table/columnResize.js +117 -0
  34. package/dist/components/table/index.d.ts +2 -0
  35. package/dist/components/table/index.js +1 -0
  36. package/dist/components/variant/VariantMatrixPicker.svelte +181 -0
  37. package/dist/components/variant/VariantMatrixPicker.svelte.d.ts +37 -0
  38. package/dist/components/variant/variantCombinations.d.ts +75 -0
  39. package/dist/components/variant/variantCombinations.js +100 -0
  40. package/dist/index.d.ts +28 -0
  41. package/dist/index.js +25 -0
  42. package/dist/styles/motion.css +100 -0
  43. package/dist/utils/keyboardShortcutRegistry.d.ts +152 -0
  44. package/dist/utils/keyboardShortcutRegistry.js +365 -0
  45. 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?` |
@@ -71,11 +72,15 @@ Then import:
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
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. 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?` |
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
@@ -195,7 +202,26 @@ const callbacks: AnnotationCallbacks = {
195
202
 
196
203
  | Component | Purpose | Key Props |
197
204
  |-----------|---------|-----------|
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) |
205
+ | `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). Precedence: `sections` > `registry` > `shortcuts`. | `shortcuts?: KeyboardShortcut[]`, `sections?: KeyboardShortcutSection[]`, `registry?: ShortcutRegistry`, `title?`, `open` (bindable) |
206
+
207
+ **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).
208
+
209
+ ```svelte
210
+ <script>
211
+ import { createShortcutRegistry, KeyboardShortcutsOverlay, SIDEBAR_TOGGLE_KEY } from "jatui"
212
+ const shortcuts = createShortcutRegistry({ contextLabels: { list: "Task list" } })
213
+ $effect(() => shortcuts.attach()) // keydown on window
214
+ $effect(() => shortcuts.pushContext("list"))
215
+ $effect(() => shortcuts.register([
216
+ { key: "j", description: "Next", context: "list", action: next },
217
+ { key: "mod+k", description: "Search", action: openPalette, allowInInput: true },
218
+ { key: SIDEBAR_TOGGLE_KEY, description: "Toggle sidebar", action: toggleSidebar },
219
+ ]))
220
+ </script>
221
+ <KeyboardShortcutsOverlay registry={shortcuts} />
222
+ ```
223
+
224
+ **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
225
 
200
226
  ### Maps (Google Maps)
201
227
 
@@ -258,6 +284,20 @@ import { qrMatrix, qrCanvas } from "jatui" // deep-li
258
284
  import { createHistory, pushState, undo, redo, beginChange, endChange, untracked, clearHistory, MAX_UNDO_DEPTH } from "jatui"
259
285
  import type { History } from "jatui"
260
286
 
287
+ // Batch/queue primitives (jatui-m4uz3.7) — companions to BatchJobTable.
288
+ import { useElapsedEstimate } from "jatui" // rune: eta.record(ms); eta.remainingMs(queued, runningElapsed, concurrency)
289
+ import { pollOperation, PollTimeoutError } from "jatui"
290
+ // await pollOperation(async ({ signal, attempt }) => op.done ? { done: true, value } : { done: false, progress },
291
+ // { intervalMs, timeoutMs, backoff, signal, onProgress }) — rejects on timeout / abort / thrown check
292
+ import { formatElapsed, rollingAverage, estimateRemainingMs } from "jatui"
293
+
294
+ // Variant combination algebra (jatui-m4uz3.8) — pure functions behind VariantMatrixPicker.
295
+ import { generateCombinations, countCombinations, type VariantCategory, type VariantSelection } from "jatui"
296
+ // VariantCategory — { id, label, icon?, mode?: 'multiplicative' | 'additive', options: VariantOption[] }
297
+ // VariantOption — { id, label, prompt? }
298
+ // VariantSelection — { [categoryId]: string[] } (selected option ids per category)
299
+ // generateCombinations(categories, selected, { forceMode? }) → [{ modifierIds, prompt, options }]
300
+
261
301
  // Date formatting
262
302
  import { formatRelativeTime, formatDate, formatShortDate, formatDateTime } from "jatui"
263
303
 
@@ -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
+ }