@joewinke/jatui 0.21.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.
- package/CLAUDE.md +47 -1
- package/dist/actions/reveal.d.ts +37 -0
- package/dist/actions/reveal.js +42 -0
- package/dist/components/AudioRecorder.svelte +749 -0
- package/dist/components/AudioRecorder.svelte.d.ts +36 -0
- package/dist/components/IconSelector.svelte +417 -0
- package/dist/components/IconSelector.svelte.d.ts +55 -0
- package/dist/components/InfiniteGrid.svelte +305 -0
- package/dist/components/InfiniteGrid.svelte.d.ts +71 -0
- package/dist/components/InfiniteSearchGrid.svelte +131 -0
- package/dist/components/InfiniteSearchGrid.svelte.d.ts +67 -0
- package/dist/components/KeyboardShortcutsOverlay.svelte +40 -3
- package/dist/components/KeyboardShortcutsOverlay.svelte.d.ts +31 -16
- package/dist/components/MediaCaptureIcon.svelte +23 -0
- package/dist/components/MediaCaptureIcon.svelte.d.ts +1 -1
- package/dist/components/ThumbnailGrid.svelte +137 -0
- package/dist/components/ThumbnailGrid.svelte.d.ts +42 -0
- package/dist/components/batch/BatchJobTable.svelte +549 -0
- package/dist/components/batch/BatchJobTable.svelte.d.ts +114 -0
- package/dist/components/batch/QuotaMeter.svelte +78 -0
- package/dist/components/batch/QuotaMeter.svelte.d.ts +16 -0
- package/dist/components/batch/batchQueue.d.ts +56 -0
- package/dist/components/batch/batchQueue.js +81 -0
- package/dist/components/batch/elapsedEstimate.d.ts +24 -0
- package/dist/components/batch/elapsedEstimate.js +25 -0
- package/dist/components/batch/pollOperation.d.ts +45 -0
- package/dist/components/batch/pollOperation.js +80 -0
- package/dist/components/batch/useElapsedEstimate.d.ts +10 -0
- package/dist/components/batch/useElapsedEstimate.js +38 -0
- package/dist/components/infiniteGridLoader.d.ts +70 -0
- package/dist/components/infiniteGridLoader.js +141 -0
- package/dist/components/table/columnResize.d.ts +38 -0
- package/dist/components/table/columnResize.js +117 -0
- package/dist/components/table/index.d.ts +2 -0
- package/dist/components/table/index.js +1 -0
- package/dist/components/variant/VariantMatrixPicker.svelte +181 -0
- package/dist/components/variant/VariantMatrixPicker.svelte.d.ts +37 -0
- package/dist/components/variant/variantCombinations.d.ts +75 -0
- package/dist/components/variant/variantCombinations.js +100 -0
- package/dist/history.d.ts +44 -0
- package/dist/history.js +87 -0
- package/dist/index.d.ts +30 -0
- package/dist/index.js +27 -0
- package/dist/styles/motion.css +100 -0
- package/dist/utils/keyboardShortcutRegistry.d.ts +152 -0
- package/dist/utils/keyboardShortcutRegistry.js +365 -0
- package/package.json +5 -1
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export interface ColumnResizeOptions {
|
|
2
|
+
/** When true, the resize handle is not added and nothing happens. Default: false */
|
|
3
|
+
disabled?: boolean;
|
|
4
|
+
/** Minimum column width in pixels. Default: 40 */
|
|
5
|
+
minWidth?: number;
|
|
6
|
+
/** Called on mousedown — use this to show the guide line (e.g. set resizeGuideX state) */
|
|
7
|
+
onResizeStart?: (initialX: number) => void;
|
|
8
|
+
/** Called on every mousemove with the new width */
|
|
9
|
+
onResize: (width: number) => void;
|
|
10
|
+
/** Called on mouseup with the final width — use this to save settings and hide the guide */
|
|
11
|
+
onResizeEnd?: (width: number) => void;
|
|
12
|
+
/**
|
|
13
|
+
* Return the container element that holds the `.resize-guide` element.
|
|
14
|
+
* If provided, the guide line position is updated on each mousemove via direct DOM manipulation,
|
|
15
|
+
* avoiding reactive overhead.
|
|
16
|
+
*/
|
|
17
|
+
getGuideContainer?: () => HTMLElement | null;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Svelte action that attaches a column-resize handle to a `<th>` (or any element).
|
|
21
|
+
*
|
|
22
|
+
* Usage:
|
|
23
|
+
* ```svelte
|
|
24
|
+
* <th use:columnResize={{
|
|
25
|
+
* minWidth: col.minWidth,
|
|
26
|
+
* onResize: (w) => { columnWidths[col.id] = w; },
|
|
27
|
+
* onResizeEnd: (w) => { saveColumnSettings(); },
|
|
28
|
+
* getGuideContainer: () => tableContainerEl,
|
|
29
|
+
* }}>
|
|
30
|
+
* ```
|
|
31
|
+
*
|
|
32
|
+
* The action appends a 4px-wide resize handle div at the right edge of the host element.
|
|
33
|
+
* Dragging it fires `onResize` continuously and `onResizeEnd` on mouse-up.
|
|
34
|
+
*/
|
|
35
|
+
export declare function columnResize(node: HTMLElement, options: ColumnResizeOptions): {
|
|
36
|
+
update(opts: ColumnResizeOptions): void;
|
|
37
|
+
destroy(): void;
|
|
38
|
+
};
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Svelte action that attaches a column-resize handle to a `<th>` (or any element).
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* ```svelte
|
|
6
|
+
* <th use:columnResize={{
|
|
7
|
+
* minWidth: col.minWidth,
|
|
8
|
+
* onResize: (w) => { columnWidths[col.id] = w; },
|
|
9
|
+
* onResizeEnd: (w) => { saveColumnSettings(); },
|
|
10
|
+
* getGuideContainer: () => tableContainerEl,
|
|
11
|
+
* }}>
|
|
12
|
+
* ```
|
|
13
|
+
*
|
|
14
|
+
* The action appends a 4px-wide resize handle div at the right edge of the host element.
|
|
15
|
+
* Dragging it fires `onResize` continuously and `onResizeEnd` on mouse-up.
|
|
16
|
+
*/
|
|
17
|
+
export function columnResize(node, options) {
|
|
18
|
+
let opts = options;
|
|
19
|
+
// If disabled, return a no-op handle
|
|
20
|
+
if (opts.disabled) {
|
|
21
|
+
return {
|
|
22
|
+
update(newOptions) { opts = newOptions; },
|
|
23
|
+
destroy() { },
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
// Build the resize handle element
|
|
27
|
+
const handle = document.createElement('div');
|
|
28
|
+
handle.className = 'col-resize-handle';
|
|
29
|
+
handle.style.cssText = [
|
|
30
|
+
'position: absolute',
|
|
31
|
+
'right: 0',
|
|
32
|
+
'top: 0',
|
|
33
|
+
'bottom: 0',
|
|
34
|
+
'width: 5px',
|
|
35
|
+
'cursor: col-resize',
|
|
36
|
+
'z-index: 2',
|
|
37
|
+
].join(';');
|
|
38
|
+
// Make the host element position-relative if it isn't already
|
|
39
|
+
const hostPosition = getComputedStyle(node).position;
|
|
40
|
+
if (hostPosition === 'static') {
|
|
41
|
+
node.style.position = 'relative';
|
|
42
|
+
}
|
|
43
|
+
node.appendChild(handle);
|
|
44
|
+
// Resize tracking state
|
|
45
|
+
let startX = 0;
|
|
46
|
+
let startWidth = 0;
|
|
47
|
+
let currentWidth = 0;
|
|
48
|
+
let resizing = false;
|
|
49
|
+
function updateGuide(clientX) {
|
|
50
|
+
const container = opts.getGuideContainer?.();
|
|
51
|
+
if (!container)
|
|
52
|
+
return;
|
|
53
|
+
const guide = container.querySelector('.resize-guide');
|
|
54
|
+
if (!guide)
|
|
55
|
+
return;
|
|
56
|
+
const rect = container.getBoundingClientRect();
|
|
57
|
+
guide.style.left = `${clientX - rect.left + container.scrollLeft}px`;
|
|
58
|
+
}
|
|
59
|
+
function onMouseMove(e) {
|
|
60
|
+
const minWidth = opts.minWidth ?? 40;
|
|
61
|
+
const diff = e.clientX - startX;
|
|
62
|
+
const newWidth = Math.max(minWidth, startWidth + diff);
|
|
63
|
+
currentWidth = newWidth;
|
|
64
|
+
opts.onResize(newWidth);
|
|
65
|
+
updateGuide(e.clientX);
|
|
66
|
+
}
|
|
67
|
+
function onMouseUp() {
|
|
68
|
+
document.body.style.cursor = '';
|
|
69
|
+
document.body.style.userSelect = '';
|
|
70
|
+
// Call onResizeEnd — the parent is responsible for clearing the guide line
|
|
71
|
+
// (typically by setting resizeGuideX = null in its onResizeEnd handler)
|
|
72
|
+
opts.onResizeEnd?.(currentWidth);
|
|
73
|
+
document.removeEventListener('mousemove', onMouseMove);
|
|
74
|
+
document.removeEventListener('mouseup', onMouseUp);
|
|
75
|
+
// Suppress the click event that fires after mouseup on the host <th>,
|
|
76
|
+
// which would otherwise trigger a column sort.
|
|
77
|
+
resizing = true;
|
|
78
|
+
requestAnimationFrame(() => { resizing = false; });
|
|
79
|
+
}
|
|
80
|
+
function onMouseDown(e) {
|
|
81
|
+
e.preventDefault();
|
|
82
|
+
e.stopPropagation();
|
|
83
|
+
startX = e.clientX;
|
|
84
|
+
startWidth = node.offsetWidth;
|
|
85
|
+
currentWidth = startWidth;
|
|
86
|
+
document.body.style.cursor = 'col-resize';
|
|
87
|
+
document.body.style.userSelect = 'none';
|
|
88
|
+
opts.onResizeStart?.(e.clientX);
|
|
89
|
+
updateGuide(e.clientX);
|
|
90
|
+
document.addEventListener('mousemove', onMouseMove);
|
|
91
|
+
document.addEventListener('mouseup', onMouseUp);
|
|
92
|
+
}
|
|
93
|
+
// Suppress click events on the host element that fire right after a resize ends.
|
|
94
|
+
// Without this, mouseup after dragging the resize handle triggers the host's onclick (e.g. sort).
|
|
95
|
+
function onHostClick(e) {
|
|
96
|
+
if (resizing) {
|
|
97
|
+
e.stopImmediatePropagation();
|
|
98
|
+
e.preventDefault();
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
handle.addEventListener('mousedown', onMouseDown);
|
|
102
|
+
node.addEventListener('click', onHostClick, true);
|
|
103
|
+
return {
|
|
104
|
+
update(newOptions) {
|
|
105
|
+
opts = newOptions;
|
|
106
|
+
},
|
|
107
|
+
destroy() {
|
|
108
|
+
handle.removeEventListener('mousedown', onMouseDown);
|
|
109
|
+
node.removeEventListener('click', onHostClick, true);
|
|
110
|
+
document.removeEventListener('mousemove', onMouseMove);
|
|
111
|
+
document.removeEventListener('mouseup', onMouseUp);
|
|
112
|
+
if (handle.parentNode === node) {
|
|
113
|
+
node.removeChild(handle);
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
export { default as DataTable } from './DataTable.svelte';
|
|
2
2
|
export { flexRender } from './flexRender';
|
|
3
|
+
export { columnResize } from './columnResize';
|
|
4
|
+
export type { ColumnResizeOptions } from './columnResize';
|
|
3
5
|
export type { DataTableProps, SelectionEvent, ColumnDef, Row } from './types';
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* VariantMatrixPicker Component
|
|
4
|
+
*
|
|
5
|
+
* Category/option chip grid for building a combinatorial variant matrix
|
|
6
|
+
* (jatui-m4uz3.8, ported from vid-gen's FrameVariantGenerator selection UI,
|
|
7
|
+
* lines 72–197 / 500–620, with the hardcoded presets stripped).
|
|
8
|
+
*
|
|
9
|
+
* Every category renders as a tab with its options as toggleable chips.
|
|
10
|
+
* Each category carries an `additive`/`multiplicative` badge:
|
|
11
|
+
* - multiplicative: each selection multiplies the total combination count
|
|
12
|
+
* (cartesian product across multiplicative categories);
|
|
13
|
+
* - additive: selections stack flat onto every combination.
|
|
14
|
+
* A live combination count renders in the footer via `countCombinations`.
|
|
15
|
+
*
|
|
16
|
+
* The algebra lives in `./variantCombinations.ts` (pure, unit-tested);
|
|
17
|
+
* the host passes `categories` and reads/binds `selected`.
|
|
18
|
+
*/
|
|
19
|
+
import {
|
|
20
|
+
countCombinations,
|
|
21
|
+
variantCategoryMode,
|
|
22
|
+
type VariantCategory,
|
|
23
|
+
type VariantSelection,
|
|
24
|
+
type VariantSelectionMode
|
|
25
|
+
} from './variantCombinations';
|
|
26
|
+
|
|
27
|
+
interface Props {
|
|
28
|
+
/** Category/option matrix — caller-supplied, no presets inside the component. */
|
|
29
|
+
categories: VariantCategory[];
|
|
30
|
+
/** categoryId → selected option ids. Bind to drive the matrix externally. */
|
|
31
|
+
selected: VariantSelection;
|
|
32
|
+
/** Force every category to one mode (`null` respects each category's own). */
|
|
33
|
+
forceMode?: VariantSelectionMode | null;
|
|
34
|
+
/** Show the live combination count footer (default true). */
|
|
35
|
+
showCount?: boolean;
|
|
36
|
+
/** Optional label for the count footer (default "combinations"). */
|
|
37
|
+
countLabel?: string;
|
|
38
|
+
/** Fired after every selection change with the new selection. */
|
|
39
|
+
onChange?: (selected: VariantSelection) => void;
|
|
40
|
+
/** Additional CSS class for the container. */
|
|
41
|
+
class?: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
let {
|
|
45
|
+
categories,
|
|
46
|
+
selected = $bindable({}),
|
|
47
|
+
forceMode = null,
|
|
48
|
+
showCount = true,
|
|
49
|
+
countLabel = 'combinations',
|
|
50
|
+
onChange,
|
|
51
|
+
class: className = ''
|
|
52
|
+
}: Props = $props();
|
|
53
|
+
|
|
54
|
+
let activeTab = $state('');
|
|
55
|
+
|
|
56
|
+
// Follow the category list if it changes identity.
|
|
57
|
+
$effect(() => {
|
|
58
|
+
if (!categories.some((c) => c.id === activeTab)) {
|
|
59
|
+
activeTab = categories[0]?.id ?? '';
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const combinationCount = $derived(countCombinations(categories, selected, forceMode));
|
|
64
|
+
const selectedCount = (categoryId: string) => selected[categoryId]?.length ?? 0;
|
|
65
|
+
const activeCategory = $derived(categories.find((c) => c.id === activeTab));
|
|
66
|
+
|
|
67
|
+
function toggleOption(categoryId: string, optionId: string) {
|
|
68
|
+
const current = selected[categoryId] ?? [];
|
|
69
|
+
const next = current.includes(optionId)
|
|
70
|
+
? current.filter((id) => id !== optionId)
|
|
71
|
+
: [...current, optionId];
|
|
72
|
+
selected = { ...selected, [categoryId]: next };
|
|
73
|
+
onChange?.(selected);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function selectAll(categoryId: string) {
|
|
77
|
+
const category = categories.find((c) => c.id === categoryId);
|
|
78
|
+
if (!category) return;
|
|
79
|
+
selected = { ...selected, [categoryId]: category.options.map((o) => o.id) };
|
|
80
|
+
onChange?.(selected);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function clearCategory(categoryId: string) {
|
|
84
|
+
selected = { ...selected, [categoryId]: [] };
|
|
85
|
+
onChange?.(selected);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function clearAll() {
|
|
89
|
+
const next: VariantSelection = {};
|
|
90
|
+
for (const c of categories) next[c.id] = [];
|
|
91
|
+
selected = next;
|
|
92
|
+
onChange?.(next);
|
|
93
|
+
}
|
|
94
|
+
</script>
|
|
95
|
+
|
|
96
|
+
<div class="{className}">
|
|
97
|
+
<!-- Category tabs: icon, label, live selection count -->
|
|
98
|
+
<div role="tablist" aria-label="Variant categories" class="flex flex-wrap gap-1 mb-3">
|
|
99
|
+
{#each categories as category (category.id)}
|
|
100
|
+
{@const count = selectedCount(category.id)}
|
|
101
|
+
<button
|
|
102
|
+
role="tab"
|
|
103
|
+
aria-selected={activeTab === category.id}
|
|
104
|
+
class="btn btn-sm gap-2 {activeTab === category.id ? 'btn-primary' : 'btn-ghost'}"
|
|
105
|
+
onclick={() => (activeTab = category.id)}
|
|
106
|
+
>
|
|
107
|
+
{#if category.icon}<span class="text-base">{category.icon}</span>{/if}
|
|
108
|
+
<span>{category.label}</span>
|
|
109
|
+
{#if count > 0}<span class="badge badge-sm">{count}</span>{/if}
|
|
110
|
+
</button>
|
|
111
|
+
{/each}
|
|
112
|
+
</div>
|
|
113
|
+
|
|
114
|
+
{#if activeCategory}
|
|
115
|
+
{@const count = selectedCount(activeCategory.id)}
|
|
116
|
+
{@const isAdditive = variantCategoryMode(activeCategory, forceMode) === 'additive'}
|
|
117
|
+
<div class="card bg-base-200 shadow-sm">
|
|
118
|
+
<div class="card-body p-4">
|
|
119
|
+
<!-- Category header: mode badge + bulk actions -->
|
|
120
|
+
<div class="flex items-center justify-between gap-2 mb-3">
|
|
121
|
+
<div class="flex items-center gap-2">
|
|
122
|
+
{#if activeCategory.icon}<span class="text-xl">{activeCategory.icon}</span>{/if}
|
|
123
|
+
<h3 class="font-bold">{activeCategory.label}</h3>
|
|
124
|
+
<span
|
|
125
|
+
class="badge badge-sm {isAdditive ? 'badge-success' : 'badge-info'}"
|
|
126
|
+
title={isAdditive
|
|
127
|
+
? 'Additive — selections stack onto every combination'
|
|
128
|
+
: 'Multiplicative — each selection multiplies the combination count'}
|
|
129
|
+
>
|
|
130
|
+
{isAdditive ? '➕ Additive' : '✖️ Multiplicative'}
|
|
131
|
+
</span>
|
|
132
|
+
{#if count > 0}<span class="badge badge-sm badge-primary">{count}</span>{/if}
|
|
133
|
+
</div>
|
|
134
|
+
<div class="flex items-center gap-1">
|
|
135
|
+
<button
|
|
136
|
+
class="btn btn-xs"
|
|
137
|
+
onclick={() => selectAll(activeCategory.id)}
|
|
138
|
+
disabled={activeCategory.options.length === 0}
|
|
139
|
+
>
|
|
140
|
+
Select all
|
|
141
|
+
</button>
|
|
142
|
+
{#if count > 0}
|
|
143
|
+
<button class="btn btn-xs btn-ghost" onclick={() => clearCategory(activeCategory.id)}>
|
|
144
|
+
Clear
|
|
145
|
+
</button>
|
|
146
|
+
{/if}
|
|
147
|
+
</div>
|
|
148
|
+
</div>
|
|
149
|
+
|
|
150
|
+
<!-- Option chip grid -->
|
|
151
|
+
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-2">
|
|
152
|
+
{#each activeCategory.options as option (option.id)}
|
|
153
|
+
{@const isSelected = selected[activeCategory.id]?.includes(option.id)}
|
|
154
|
+
<button
|
|
155
|
+
class="btn btn-sm normal-case justify-start gap-1 {isSelected
|
|
156
|
+
? 'btn-info'
|
|
157
|
+
: 'btn-outline'}"
|
|
158
|
+
aria-pressed={isSelected}
|
|
159
|
+
onclick={() => toggleOption(activeCategory.id, option.id)}
|
|
160
|
+
>
|
|
161
|
+
{isSelected ? '✓' : '➕'}
|
|
162
|
+
<span class="truncate">{option.label}</span>
|
|
163
|
+
</button>
|
|
164
|
+
{/each}
|
|
165
|
+
</div>
|
|
166
|
+
</div>
|
|
167
|
+
</div>
|
|
168
|
+
{/if}
|
|
169
|
+
|
|
170
|
+
<!-- Footer: live combination count -->
|
|
171
|
+
{#if showCount}
|
|
172
|
+
<div class="flex items-center justify-between mt-3 text-sm">
|
|
173
|
+
<span class="text-base-content/60">
|
|
174
|
+
<strong class="text-base-content">{combinationCount}</strong>
|
|
175
|
+
{countLabel}
|
|
176
|
+
{#if combinationCount === 0}<span class="text-base-content/40"> — select an option to start</span>{/if}
|
|
177
|
+
</span>
|
|
178
|
+
<button class="btn btn-xs btn-ghost" onclick={clearAll}>Clear all</button>
|
|
179
|
+
</div>
|
|
180
|
+
{/if}
|
|
181
|
+
</div>
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VariantMatrixPicker Component
|
|
3
|
+
*
|
|
4
|
+
* Category/option chip grid for building a combinatorial variant matrix
|
|
5
|
+
* (jatui-m4uz3.8, ported from vid-gen's FrameVariantGenerator selection UI,
|
|
6
|
+
* lines 72–197 / 500–620, with the hardcoded presets stripped).
|
|
7
|
+
*
|
|
8
|
+
* Every category renders as a tab with its options as toggleable chips.
|
|
9
|
+
* Each category carries an `additive`/`multiplicative` badge:
|
|
10
|
+
* - multiplicative: each selection multiplies the total combination count
|
|
11
|
+
* (cartesian product across multiplicative categories);
|
|
12
|
+
* - additive: selections stack flat onto every combination.
|
|
13
|
+
* A live combination count renders in the footer via `countCombinations`.
|
|
14
|
+
*
|
|
15
|
+
* The algebra lives in `./variantCombinations.ts` (pure, unit-tested);
|
|
16
|
+
* the host passes `categories` and reads/binds `selected`.
|
|
17
|
+
*/
|
|
18
|
+
import { type VariantCategory, type VariantSelection, type VariantSelectionMode } from './variantCombinations';
|
|
19
|
+
interface Props {
|
|
20
|
+
/** Category/option matrix — caller-supplied, no presets inside the component. */
|
|
21
|
+
categories: VariantCategory[];
|
|
22
|
+
/** categoryId → selected option ids. Bind to drive the matrix externally. */
|
|
23
|
+
selected: VariantSelection;
|
|
24
|
+
/** Force every category to one mode (`null` respects each category's own). */
|
|
25
|
+
forceMode?: VariantSelectionMode | null;
|
|
26
|
+
/** Show the live combination count footer (default true). */
|
|
27
|
+
showCount?: boolean;
|
|
28
|
+
/** Optional label for the count footer (default "combinations"). */
|
|
29
|
+
countLabel?: string;
|
|
30
|
+
/** Fired after every selection change with the new selection. */
|
|
31
|
+
onChange?: (selected: VariantSelection) => void;
|
|
32
|
+
/** Additional CSS class for the container. */
|
|
33
|
+
class?: string;
|
|
34
|
+
}
|
|
35
|
+
declare const VariantMatrixPicker: import("svelte").Component<Props, {}, "selected">;
|
|
36
|
+
type VariantMatrixPicker = ReturnType<typeof VariantMatrixPicker>;
|
|
37
|
+
export default VariantMatrixPicker;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure combination algebra behind `VariantMatrixPicker` (jatui-m4uz3.8).
|
|
3
|
+
*
|
|
4
|
+
* Ported from vid-gen's `variantPresets.js` (generateCombinations + friends),
|
|
5
|
+
* with the hardcoded apparel/background/lighting preset map stripped out:
|
|
6
|
+
* every category and option is caller-supplied, so the algebra works for any
|
|
7
|
+
* combinatorial domain (image variants, A/B matrices, test permutations…).
|
|
8
|
+
*
|
|
9
|
+
* Two category modes drive the math:
|
|
10
|
+
* - `multiplicative`: each selected option pairs with every selection in the
|
|
11
|
+
* other multiplicative categories → cartesian product, one new variant
|
|
12
|
+
* per pick.
|
|
13
|
+
* - `additive`: all selected options stack flat into every combination —
|
|
14
|
+
* they ride along instead of multiplying.
|
|
15
|
+
*
|
|
16
|
+
* No DOM, no runes — kept separate so the math is unit-testable under plain
|
|
17
|
+
* node:test (mirrors the `batch/` module split from jatui-m4uz3.7).
|
|
18
|
+
*/
|
|
19
|
+
export type VariantSelectionMode = 'multiplicative' | 'additive';
|
|
20
|
+
export interface VariantOption {
|
|
21
|
+
id: string;
|
|
22
|
+
label: string;
|
|
23
|
+
/** Optional host payload — vid-gen used image-edit prompts; joined with ', ' into `VariantCombination.prompt`. */
|
|
24
|
+
prompt?: string;
|
|
25
|
+
}
|
|
26
|
+
export interface VariantCategory {
|
|
27
|
+
id: string;
|
|
28
|
+
label: string;
|
|
29
|
+
/** Emoji or short glyph shown on the category tab. */
|
|
30
|
+
icon?: string;
|
|
31
|
+
/** Defaults to `multiplicative`. */
|
|
32
|
+
mode?: VariantSelectionMode;
|
|
33
|
+
options: VariantOption[];
|
|
34
|
+
}
|
|
35
|
+
/** categoryId → selected option ids (mirror of vid-gen's selectedModifiers). */
|
|
36
|
+
export type VariantSelection = Record<string, string[]>;
|
|
37
|
+
export interface VariantCombination {
|
|
38
|
+
/** Flat list of every option id in the combination (multiplicative picks + stacked additive picks). */
|
|
39
|
+
modifierIds: string[];
|
|
40
|
+
/** All `prompt`s joined with ', ' — empty string when no option carries a prompt. */
|
|
41
|
+
prompt: string;
|
|
42
|
+
/** Resolved option objects, in category order. */
|
|
43
|
+
options: VariantOption[];
|
|
44
|
+
}
|
|
45
|
+
export interface GenerateCombinationsOptions {
|
|
46
|
+
/** Override every category's mode. `null` (default) respects each category's own mode. */
|
|
47
|
+
forceMode?: VariantSelectionMode | null;
|
|
48
|
+
}
|
|
49
|
+
/** Resolve a category's effective mode: forceMode wins, then the category's own, then multiplicative. */
|
|
50
|
+
export declare function variantCategoryMode(category: VariantCategory, forceMode?: VariantSelectionMode | null): VariantSelectionMode;
|
|
51
|
+
/** Look up an option by category + id across a caller-supplied category list. */
|
|
52
|
+
export declare function findVariantOption(categories: VariantCategory[], categoryId: string, optionId: string): VariantOption | undefined;
|
|
53
|
+
/**
|
|
54
|
+
* Number of combinations `generateCombinations` would produce — the live count
|
|
55
|
+
* the picker footer renders. Multiplicative categories multiply; a selection
|
|
56
|
+
* with only additive categories counts as exactly 1 stacked combination.
|
|
57
|
+
*/
|
|
58
|
+
export declare function countCombinations(categories: VariantCategory[], selected: VariantSelection, forceMode?: VariantSelectionMode | null): number;
|
|
59
|
+
/**
|
|
60
|
+
* Generate all combinations from the current selection.
|
|
61
|
+
*
|
|
62
|
+
* Mirrors vid-gen's `generateCombinations(selectedModifiers, options)`:
|
|
63
|
+
* - categories with nothing selected are skipped entirely (all-empty → []);
|
|
64
|
+
* - multiplicative categories contribute one option per combination via
|
|
65
|
+
* cartesian product; additive selections stack flat onto every combination;
|
|
66
|
+
* - with no multiplicative selections at all, a single additive-only
|
|
67
|
+
* combination is produced.
|
|
68
|
+
*/
|
|
69
|
+
export declare function generateCombinations(categories: VariantCategory[], selected: VariantSelection, options?: GenerateCombinationsOptions): VariantCombination[];
|
|
70
|
+
/**
|
|
71
|
+
* Build a variant filename from an original filename and modifier ids
|
|
72
|
+
* (vid-gen's `generateVariantFilename`, generic — no preset knowledge):
|
|
73
|
+
* `photo.png` + ['a', 'b'] → `photo_a_b.png`, ids sorted for consistency.
|
|
74
|
+
*/
|
|
75
|
+
export declare function generateVariantFilename(originalFilename: string, modifierIds: string[]): string;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure combination algebra behind `VariantMatrixPicker` (jatui-m4uz3.8).
|
|
3
|
+
*
|
|
4
|
+
* Ported from vid-gen's `variantPresets.js` (generateCombinations + friends),
|
|
5
|
+
* with the hardcoded apparel/background/lighting preset map stripped out:
|
|
6
|
+
* every category and option is caller-supplied, so the algebra works for any
|
|
7
|
+
* combinatorial domain (image variants, A/B matrices, test permutations…).
|
|
8
|
+
*
|
|
9
|
+
* Two category modes drive the math:
|
|
10
|
+
* - `multiplicative`: each selected option pairs with every selection in the
|
|
11
|
+
* other multiplicative categories → cartesian product, one new variant
|
|
12
|
+
* per pick.
|
|
13
|
+
* - `additive`: all selected options stack flat into every combination —
|
|
14
|
+
* they ride along instead of multiplying.
|
|
15
|
+
*
|
|
16
|
+
* No DOM, no runes — kept separate so the math is unit-testable under plain
|
|
17
|
+
* node:test (mirrors the `batch/` module split from jatui-m4uz3.7).
|
|
18
|
+
*/
|
|
19
|
+
/** Resolve a category's effective mode: forceMode wins, then the category's own, then multiplicative. */
|
|
20
|
+
export function variantCategoryMode(category, forceMode = null) {
|
|
21
|
+
return forceMode ?? category.mode ?? 'multiplicative';
|
|
22
|
+
}
|
|
23
|
+
/** Look up an option by category + id across a caller-supplied category list. */
|
|
24
|
+
export function findVariantOption(categories, categoryId, optionId) {
|
|
25
|
+
return categories.find((c) => c.id === categoryId)?.options.find((o) => o.id === optionId);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Number of combinations `generateCombinations` would produce — the live count
|
|
29
|
+
* the picker footer renders. Multiplicative categories multiply; a selection
|
|
30
|
+
* with only additive categories counts as exactly 1 stacked combination.
|
|
31
|
+
*/
|
|
32
|
+
export function countCombinations(categories, selected, forceMode = null) {
|
|
33
|
+
const active = categories.filter((c) => (selected[c.id]?.length ?? 0) > 0);
|
|
34
|
+
if (active.length === 0)
|
|
35
|
+
return 0;
|
|
36
|
+
let count = 1;
|
|
37
|
+
let multiplicativeSeen = false;
|
|
38
|
+
for (const category of active) {
|
|
39
|
+
const n = selected[category.id].length;
|
|
40
|
+
if (variantCategoryMode(category, forceMode) === 'multiplicative') {
|
|
41
|
+
multiplicativeSeen = true;
|
|
42
|
+
count *= n;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
// Only-additive selections produce a single stacked combination, not zero.
|
|
46
|
+
return multiplicativeSeen ? count : 1;
|
|
47
|
+
}
|
|
48
|
+
/** Cartesian product of arrays (vid-gen's helper, verbatim semantics). */
|
|
49
|
+
function cartesian(...arrays) {
|
|
50
|
+
return arrays.reduce((acc, array) => acc.flatMap((x) => array.map((y) => [...x, y])), [[]]);
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Generate all combinations from the current selection.
|
|
54
|
+
*
|
|
55
|
+
* Mirrors vid-gen's `generateCombinations(selectedModifiers, options)`:
|
|
56
|
+
* - categories with nothing selected are skipped entirely (all-empty → []);
|
|
57
|
+
* - multiplicative categories contribute one option per combination via
|
|
58
|
+
* cartesian product; additive selections stack flat onto every combination;
|
|
59
|
+
* - with no multiplicative selections at all, a single additive-only
|
|
60
|
+
* combination is produced.
|
|
61
|
+
*/
|
|
62
|
+
export function generateCombinations(categories, selected, options = {}) {
|
|
63
|
+
const { forceMode = null } = options;
|
|
64
|
+
const active = categories.filter((c) => (selected[c.id]?.length ?? 0) > 0);
|
|
65
|
+
if (active.length === 0)
|
|
66
|
+
return [];
|
|
67
|
+
const multiplicativeArrays = active
|
|
68
|
+
.filter((c) => variantCategoryMode(c, forceMode) === 'multiplicative')
|
|
69
|
+
.map((c) => selected[c.id].map((optionId) => ({ categoryId: c.id, optionId })));
|
|
70
|
+
const additivePicks = active
|
|
71
|
+
.filter((c) => variantCategoryMode(c, forceMode) === 'additive')
|
|
72
|
+
.flatMap((c) => selected[c.id].map((optionId) => ({ categoryId: c.id, optionId })));
|
|
73
|
+
const multiplicativeCombos = cartesian(...multiplicativeArrays);
|
|
74
|
+
const combos = multiplicativeArrays.length
|
|
75
|
+
? multiplicativeCombos.map((combo) => [...combo, ...additivePicks])
|
|
76
|
+
: [additivePicks];
|
|
77
|
+
return combos.map((combo) => {
|
|
78
|
+
const resolved = combo
|
|
79
|
+
.map(({ categoryId, optionId }) => findVariantOption(categories, categoryId, optionId))
|
|
80
|
+
.filter((o) => o !== undefined);
|
|
81
|
+
return {
|
|
82
|
+
modifierIds: combo.map(({ optionId }) => optionId),
|
|
83
|
+
prompt: resolved
|
|
84
|
+
.map((o) => o.prompt)
|
|
85
|
+
.filter((p) => p !== undefined)
|
|
86
|
+
.join(', '),
|
|
87
|
+
options: resolved
|
|
88
|
+
};
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Build a variant filename from an original filename and modifier ids
|
|
93
|
+
* (vid-gen's `generateVariantFilename`, generic — no preset knowledge):
|
|
94
|
+
* `photo.png` + ['a', 'b'] → `photo_a_b.png`, ids sorted for consistency.
|
|
95
|
+
*/
|
|
96
|
+
export function generateVariantFilename(originalFilename, modifierIds) {
|
|
97
|
+
const stem = originalFilename.replace(/\.[^/.]+$/, '');
|
|
98
|
+
const suffix = [...modifierIds].sort().join('_');
|
|
99
|
+
return suffix ? `${stem}_${suffix}.png` : `${stem}.png`;
|
|
100
|
+
}
|
|
@@ -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;
|