@joewinke/jatui 0.20.0 → 0.22.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/CLAUDE.md CHANGED
@@ -26,7 +26,7 @@ Then import:
26
26
  | `SelectInput` | Dropdown select | `label`, `value` (bindable), `options: SelectOption[]`, `placeholder` |
27
27
  | `PhoneInput` | Intl phone (country dropdown + tel input, outputs E.164; caps national digits per country). Fires `onChange(value)` on every change. | `value` (bindable), `name`, `defaultCountry`, `required`, `onChange` |
28
28
  | `ChipInput` | Tag/chip entry with suggestions | `chips` (bindable), `suggestions: SuggestionGroup[]`, `placeholder` |
29
- | `SearchDropdown` | Searchable dropdown with grouped results, keyboard nav, avatar/initials options. `appendToBody` escapes table/overflow stacking contexts; `variant='chip'` renders a compact pill; `size='sm'` a smaller trigger; `dropup` opens upward; `clearable` shows an × when a value is set. Exposes `openDropdown()`. | `groups: SearchDropdownGroup[]`, `value`, `onChange`, `placeholder`, `variant`, `size`, `dropup`, `appendToBody`, `clearable`, `colorFn` |
29
+ | `SearchDropdown` | Searchable dropdown with grouped results, keyboard nav, avatar/initials options. `appendToBody` escapes table/overflow stacking contexts; `variant='chip'` renders a compact pill; `size='sm'` a smaller trigger; `dropup` opens upward; `clearable` shows an × when a value is set. `onSearch` turns the built-in search box into a debounced server-side search hook (fires on open/clear with `''`; internal filtering is bypassed — the consumer updates `groups`). Exposes `openDropdown()`. | `groups: SearchDropdownGroup[]`, `value`, `onChange`, `placeholder`, `variant`, `size`, `dropup`, `appendToBody`, `clearable`, `colorFn`, `onSearch`, `searchDebounceMs` |
30
30
  | `FilterDropdown` | Multi-select filter dropdown | `label`, `options: string[]`, `selected: Set<string>` (bindable) |
31
31
  | `SortDropdown` | Sort by column with direction | `options: SortOption[]`, `sortBy` (bindable), `sortDir` (bindable) |
32
32
  | `InlineEdit` | Click-to-edit text | `value` (bindable), `segments: DisplaySegment[]`, `onSave` |
@@ -252,6 +252,12 @@ import { snapResume, resumeLabel } from "jatui" // resume
252
252
  import { createQuietChain, amountFor } from "jatui" // loudness-compensated tone (WebAudio)
253
253
  import { qrMatrix, qrCanvas } from "jatui" // deep-link QR, ISO 18004 v1–10, no deps
254
254
 
255
+ // Snapshot undo/redo history (wayflow-core, generic over any JSON-shaped state G).
256
+ // Also importable as 'jatui/history'. Snapshots are JSON-deep-cloned (safe on $state proxies).
257
+ // beginChange/endChange = push-on-first-move for drag gestures; untracked() = compound op, no pushes.
258
+ import { createHistory, pushState, undo, redo, beginChange, endChange, untracked, clearHistory, MAX_UNDO_DEPTH } from "jatui"
259
+ import type { History } from "jatui"
260
+
255
261
  // Date formatting
256
262
  import { formatRelativeTime, formatDate, formatShortDate, formatDateTime } from "jatui"
257
263
 
@@ -36,6 +36,8 @@
36
36
  triggerAttrs,
37
37
  embedded = false,
38
38
  onChange,
39
+ onSearch,
40
+ searchDebounceMs = 250,
39
41
  onCreate,
40
42
  createLabel = (q: string) => `Create "${q}"`,
41
43
  }: {
@@ -65,6 +67,21 @@
65
67
  /** Render inline without a trigger button — panel content is always visible, no dropdown behavior */
66
68
  embedded?: boolean;
67
69
  onChange: (value: string) => void;
70
+ /**
71
+ * Server-side search hook (mos-ibafm). When set, the built-in search box
72
+ * drives ASYNC results instead of the client-side substring filter: every
73
+ * keystroke (debounced to `searchDebounceMs`) calls onSearch with the
74
+ * current query, and the consumer updates `groups` with the server's
75
+ * response. Also fires on panel open (typically with '') so the consumer
76
+ * can load the initial result set, and again when the query is cleared —
77
+ * so '' means "reset to the default list". Internal filtering is bypassed
78
+ * entirely while this is set: the consumer owns exactly what the list
79
+ * shows (a fuzzy server match must not be re-filtered by substring).
80
+ * With it absent, behavior is unchanged (client-side filter).
81
+ */
82
+ onSearch?: (query: string) => void;
83
+ /** Debounce applied to onSearch keystrokes before the callback fires. */
84
+ searchDebounceMs?: number;
68
85
  /**
69
86
  * When set, an always-present "create" row is rendered as the last option
70
87
  * whenever the search box is non-empty — it survives a zero-match filter (so
@@ -108,6 +125,10 @@
108
125
  const activeColor = $derived(colorFn ? colorFn(value) : undefined);
109
126
 
110
127
  const filteredGroups = $derived.by(() => {
128
+ // Server-search mode: the consumer owns the result set (groups arrive
129
+ // from onSearch responses) — re-filtering them client-side would hide
130
+ // fuzzy server matches that don't substring-match the query.
131
+ if (onSearch) return groups;
111
132
  if (!searchQuery.trim()) return groups;
112
133
  const q = searchQuery.toLowerCase();
113
134
  const result: SearchDropdownGroup[] = [];
@@ -166,6 +187,19 @@
166
187
  });
167
188
  }
168
189
 
190
+ // Debounced onSearch: one timer per open panel; each keystroke reruns the
191
+ // effect, which clears the pending timer and re-arms it — a burst of
192
+ // keystrokes collapses into a single callback firing `searchDebounceMs`
193
+ // after the last one. Firing on open too (query is '' after a reset) is
194
+ // what lets the consumer load the initial result set from the server.
195
+ $effect(() => {
196
+ if (!onSearch) return;
197
+ if (!open && !embedded) return;
198
+ const q = searchQuery;
199
+ const t = setTimeout(() => onSearch(q), searchDebounceMs);
200
+ return () => clearTimeout(t);
201
+ });
202
+
169
203
  function handleSearchKeydown(e: KeyboardEvent) {
170
204
  if (e.key === 'Escape') {
171
205
  e.stopPropagation();
@@ -36,6 +36,21 @@ type $$ComponentProps = {
36
36
  /** Render inline without a trigger button — panel content is always visible, no dropdown behavior */
37
37
  embedded?: boolean;
38
38
  onChange: (value: string) => void;
39
+ /**
40
+ * Server-side search hook (mos-ibafm). When set, the built-in search box
41
+ * drives ASYNC results instead of the client-side substring filter: every
42
+ * keystroke (debounced to `searchDebounceMs`) calls onSearch with the
43
+ * current query, and the consumer updates `groups` with the server's
44
+ * response. Also fires on panel open (typically with '') so the consumer
45
+ * can load the initial result set, and again when the query is cleared —
46
+ * so '' means "reset to the default list". Internal filtering is bypassed
47
+ * entirely while this is set: the consumer owns exactly what the list
48
+ * shows (a fuzzy server match must not be re-filtered by substring).
49
+ * With it absent, behavior is unchanged (client-side filter).
50
+ */
51
+ onSearch?: (query: string) => void;
52
+ /** Debounce applied to onSearch keystrokes before the callback fires. */
53
+ searchDebounceMs?: number;
39
54
  /**
40
55
  * When set, an always-present "create" row is rendered as the last option
41
56
  * whenever the search box is non-empty — it survives a zero-match filter (so
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Snapshot undo/redo history. Forked from @wayflow/core's history.ts (see
3
+ * README.md) and generalized to any graph type `G` — this module only
4
+ * deep-clones snapshots, it never inspects node/edge shape.
5
+ *
6
+ * Adds two things upstream keeps outside `core` (in its DOM editor package):
7
+ * push-on-first-move (`beginChange`/`endChange`) so a drag/resize gesture
8
+ * pushes exactly one pre-change snapshot no matter how many move events it
9
+ * fires, and `untracked()` to run a compound multi-step mutation (e.g.
10
+ * paste = add nodes + add edges) without each step pushing its own entry.
11
+ *
12
+ * CANONICAL HOME: this is the shared copy (jatui-00jfh, rebuilding the lost
13
+ * jat-gjw72 upstream half). The JAT IDE (ide/src/lib/utils/history.ts) and
14
+ * JST (src/lib/workflows/core/history.ts) carry vendored copies that
15
+ * repoint their imports at 'jatui' and are deleted at the swap.
16
+ */
17
+ export declare const MAX_UNDO_DEPTH = 50;
18
+ export interface History<G> {
19
+ undoStack: G[];
20
+ redoStack: G[];
21
+ /** True while a push-on-first-move gesture is in progress (see beginChange). */
22
+ changing: boolean;
23
+ /** Suppression depth for untracked(). Pushes no-op while > 0. */
24
+ suppressed: number;
25
+ }
26
+ export declare function createHistory<G>(): History<G>;
27
+ /** Deep-clones `graph` onto the undo stack (capped at maxDepth) and clears redo. No-ops while suppressed. */
28
+ export declare function pushState<G>(history: History<G>, graph: G, maxDepth?: number): void;
29
+ /**
30
+ * Push-on-first-move: call on every move event of a drag/resize gesture.
31
+ * Pushes the pre-gesture snapshot only the first time it's called since the
32
+ * last endChange() — a click that never drags won't add a history entry.
33
+ */
34
+ export declare function beginChange<G>(history: History<G>, graph: G, maxDepth?: number): void;
35
+ /** Ends the current push-on-first-move gesture so the next beginChange() can push again. */
36
+ export declare function endChange<G>(history: History<G>): void;
37
+ /** Runs `fn` with history pushes suppressed — for compound ops that should land as zero or one entries (pair with a manual pushState before calling). */
38
+ export declare function untracked<G>(history: History<G>, fn: () => void): void;
39
+ /** Pops the most recent undo entry, pushing `currentGraph` onto redo. Returns null if nothing to undo. */
40
+ export declare function undo<G>(history: History<G>, currentGraph: G): G | null;
41
+ /** Pops the most recent redo entry, pushing `currentGraph` onto undo. Returns null if nothing to redo. */
42
+ export declare function redo<G>(history: History<G>, currentGraph: G): G | null;
43
+ /** Clears both stacks and any in-progress gesture/suppression state. */
44
+ export declare function clearHistory<G>(history: History<G>): void;
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Snapshot undo/redo history. Forked from @wayflow/core's history.ts (see
3
+ * README.md) and generalized to any graph type `G` — this module only
4
+ * deep-clones snapshots, it never inspects node/edge shape.
5
+ *
6
+ * Adds two things upstream keeps outside `core` (in its DOM editor package):
7
+ * push-on-first-move (`beginChange`/`endChange`) so a drag/resize gesture
8
+ * pushes exactly one pre-change snapshot no matter how many move events it
9
+ * fires, and `untracked()` to run a compound multi-step mutation (e.g.
10
+ * paste = add nodes + add edges) without each step pushing its own entry.
11
+ *
12
+ * CANONICAL HOME: this is the shared copy (jatui-00jfh, rebuilding the lost
13
+ * jat-gjw72 upstream half). The JAT IDE (ide/src/lib/utils/history.ts) and
14
+ * JST (src/lib/workflows/core/history.ts) carry vendored copies that
15
+ * repoint their imports at 'jatui' and are deleted at the swap.
16
+ */
17
+ export const MAX_UNDO_DEPTH = 50;
18
+ /**
19
+ * Deep-clones plain JSON-shaped state. Not `structuredClone` — callers pass
20
+ * Svelte 5 `$state` proxies (e.g. the canvas's reactive nodes/edges arrays),
21
+ * and `structuredClone` throws `DataCloneError` on those in real browsers
22
+ * (verified in-browser). A JSON round-trip reads through the proxy's
23
+ * property getters like any other consumer and is safe for graph data (no
24
+ * Date/Map/Set/functions).
25
+ */
26
+ function deepClone(value) {
27
+ return JSON.parse(JSON.stringify(value));
28
+ }
29
+ export function createHistory() {
30
+ return { undoStack: [], redoStack: [], changing: false, suppressed: 0 };
31
+ }
32
+ /** Deep-clones `graph` onto the undo stack (capped at maxDepth) and clears redo. No-ops while suppressed. */
33
+ export function pushState(history, graph, maxDepth = MAX_UNDO_DEPTH) {
34
+ if (history.suppressed > 0)
35
+ return;
36
+ if (history.undoStack.length >= maxDepth) {
37
+ history.undoStack.shift();
38
+ }
39
+ history.undoStack.push(deepClone(graph));
40
+ history.redoStack = [];
41
+ }
42
+ /**
43
+ * Push-on-first-move: call on every move event of a drag/resize gesture.
44
+ * Pushes the pre-gesture snapshot only the first time it's called since the
45
+ * last endChange() — a click that never drags won't add a history entry.
46
+ */
47
+ export function beginChange(history, graph, maxDepth = MAX_UNDO_DEPTH) {
48
+ if (history.changing)
49
+ return;
50
+ history.changing = true;
51
+ pushState(history, graph, maxDepth);
52
+ }
53
+ /** Ends the current push-on-first-move gesture so the next beginChange() can push again. */
54
+ export function endChange(history) {
55
+ history.changing = false;
56
+ }
57
+ /** Runs `fn` with history pushes suppressed — for compound ops that should land as zero or one entries (pair with a manual pushState before calling). */
58
+ export function untracked(history, fn) {
59
+ history.suppressed++;
60
+ try {
61
+ fn();
62
+ }
63
+ finally {
64
+ history.suppressed--;
65
+ }
66
+ }
67
+ /** Pops the most recent undo entry, pushing `currentGraph` onto redo. Returns null if nothing to undo. */
68
+ export function undo(history, currentGraph) {
69
+ if (history.undoStack.length === 0)
70
+ return null;
71
+ history.redoStack.push(deepClone(currentGraph));
72
+ return history.undoStack.pop();
73
+ }
74
+ /** Pops the most recent redo entry, pushing `currentGraph` onto undo. Returns null if nothing to redo. */
75
+ export function redo(history, currentGraph) {
76
+ if (history.redoStack.length === 0)
77
+ return null;
78
+ history.undoStack.push(deepClone(currentGraph));
79
+ return history.redoStack.pop();
80
+ }
81
+ /** Clears both stacks and any in-progress gesture/suppression state. */
82
+ export function clearHistory(history) {
83
+ history.undoStack = [];
84
+ history.redoStack = [];
85
+ history.changing = false;
86
+ history.suppressed = 0;
87
+ }
package/dist/index.d.ts CHANGED
@@ -225,3 +225,5 @@ export { computeActiveSection, DEFAULT_ACTIVATION_RATIO } from './components/bas
225
225
  export type { SectionRect, SpyViewport, SpyScrollState } from './components/basic/sectionNavSpy';
226
226
  export { default as Slider } from './components/basic/Slider.svelte';
227
227
  export { default as Pagination } from './components/basic/Pagination.svelte';
228
+ export { MAX_UNDO_DEPTH, createHistory, pushState, beginChange, endChange, untracked, undo, redo, clearHistory } from './history';
229
+ export type { History } from './history';
package/dist/index.js CHANGED
@@ -260,3 +260,5 @@ export { default as SectionNav } from './components/basic/SectionNav.svelte';
260
260
  export { computeActiveSection, DEFAULT_ACTIVATION_RATIO } from './components/basic/sectionNavSpy';
261
261
  export { default as Slider } from './components/basic/Slider.svelte';
262
262
  export { default as Pagination } from './components/basic/Pagination.svelte';
263
+ // Snapshot undo/redo history (wayflow-core) — also importable as 'jatui/history' (jatui-00jfh).
264
+ export { MAX_UNDO_DEPTH, createHistory, pushState, beginChange, endChange, untracked, undo, redo, clearHistory } from './history';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@joewinke/jatui",
3
- "version": "0.20.0",
3
+ "version": "0.22.0",
4
4
  "private": false,
5
5
  "description": "Shared Svelte 5 component library for JAT projects",
6
6
  "type": "module",
@@ -17,6 +17,10 @@
17
17
  "svelte": "./dist/dnd/index.js",
18
18
  "default": "./dist/dnd/index.js"
19
19
  },
20
+ "./history": {
21
+ "types": "./dist/history.d.ts",
22
+ "default": "./dist/history.js"
23
+ },
20
24
  "./styles/rail.css": "./dist/styles/rail.css",
21
25
  "./showcase": {
22
26
  "types": "./dist/showcase/index.d.ts",