@joewinke/jatui 0.32.0 → 0.34.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 (28) hide show
  1. package/CLAUDE.md +16 -1
  2. package/dist/actions/reviewKeys.d.ts +159 -0
  3. package/dist/actions/reviewKeys.js +353 -0
  4. package/dist/components/VideoPlayer.svelte +437 -68
  5. package/dist/components/VideoPlayer.svelte.d.ts +17 -0
  6. package/dist/components/review/MediaZoomLightbox.svelte +628 -0
  7. package/dist/components/review/MediaZoomLightbox.svelte.d.ts +56 -0
  8. package/dist/components/review/ReviewFilterBar.svelte +296 -0
  9. package/dist/components/review/ReviewFilterBar.svelte.d.ts +45 -0
  10. package/dist/components/review/ReviewNote.svelte +213 -0
  11. package/dist/components/review/ReviewNote.svelte.d.ts +37 -0
  12. package/dist/components/review/ReviewProgress.svelte +99 -0
  13. package/dist/components/review/ReviewProgress.svelte.d.ts +32 -0
  14. package/dist/components/review/ReviewShortcuts.svelte +80 -0
  15. package/dist/components/review/ReviewShortcuts.svelte.d.ts +15 -0
  16. package/dist/components/review/RoundHistory.svelte +151 -0
  17. package/dist/components/review/RoundHistory.svelte.d.ts +35 -0
  18. package/dist/components/review/VerdictControl.svelte +163 -0
  19. package/dist/components/review/VerdictControl.svelte.d.ts +23 -0
  20. package/dist/components/review/index.d.ts +24 -0
  21. package/dist/components/review/index.js +23 -0
  22. package/dist/components/review/mediaZoom.d.ts +115 -0
  23. package/dist/components/review/mediaZoom.js +161 -0
  24. package/dist/components/review/reviewSummary.d.ts +101 -0
  25. package/dist/components/review/reviewSummary.js +122 -0
  26. package/dist/index.d.ts +14 -0
  27. package/dist/index.js +12 -0
  28. package/package.json +6 -1
@@ -0,0 +1,161 @@
1
+ /**
2
+ * mediaZoom — the pure core of MediaZoomLightbox (jatui-c79nt, ported from
3
+ * mardukos media-picker/zoom.ts, mos-0wvol).
4
+ *
5
+ * Zoom/pan math, frame stepping, view/item paging and the key table. The
6
+ * transform is `translate(x, y) scale(s)` on a wrapper whose origin is the
7
+ * stage centre, so every geometry helper works in stage-centred pixels.
8
+ * No DOM — every helper here is unit-tested in isolation.
9
+ *
10
+ * The key table (`MEDIA_ZOOM_KEYS`) is the single source for BOTH the
11
+ * keydown handler (`resolveMediaZoomKey`) and the on-screen hint
12
+ * (`mediaZoomHint`), so the documented keys can't drift from the behaviour
13
+ * (docs/review-ux/proposal.md → "One keyboard map").
14
+ */
15
+ export const MIN_ZOOM = 1;
16
+ export const MAX_ZOOM = 8;
17
+ export const ZOOM_STEP = 1.25;
18
+ export const ZOOM_IDENTITY = Object.freeze({ scale: 1, x: 0, y: 0 });
19
+ /** Frame step: the asset's fps is unknown to the page; review loops are 30fps. */
20
+ export const DEFAULT_FPS = 30;
21
+ export const DEFAULT_SPEEDS = [0.25, 0.5, 0.75, 1];
22
+ export function clampScale(s) {
23
+ if (!Number.isFinite(s))
24
+ return MIN_ZOOM;
25
+ return Math.min(MAX_ZOOM, Math.max(MIN_ZOOM, s));
26
+ }
27
+ /** Keep the content covering the stage: at scale s the slack is (s-1)·w/2. */
28
+ export function clampPan(z, w, h) {
29
+ const mx = ((z.scale - 1) * w) / 2;
30
+ const my = ((z.scale - 1) * h) / 2;
31
+ return {
32
+ scale: z.scale,
33
+ x: Math.min(mx, Math.max(-mx, z.x)),
34
+ y: Math.min(my, Math.max(-my, z.y))
35
+ };
36
+ }
37
+ /**
38
+ * Zoom by `factor` keeping the point (cx, cy) — stage-centred px — fixed
39
+ * under the pointer. Back at scale 1 the pan resets.
40
+ */
41
+ export function zoomAt(z, factor, cx, cy, w, h) {
42
+ const scale = clampScale(z.scale * factor);
43
+ if (scale === MIN_ZOOM)
44
+ return { ...ZOOM_IDENTITY };
45
+ const k = scale / z.scale;
46
+ return clampPan({ scale, x: cx - (cx - z.x) * k, y: cy - (cy - z.y) * k }, w, h);
47
+ }
48
+ export function panBy(z, dx, dy, w, h) {
49
+ if (z.scale <= MIN_ZOOM)
50
+ return z;
51
+ return clampPan({ scale: z.scale, x: z.x + dx, y: z.y + dy }, w, h);
52
+ }
53
+ /** Wheel delta → multiplicative zoom factor (smooth on trackpads too). */
54
+ export function wheelFactor(deltaY) {
55
+ return Math.exp(-deltaY * 0.0015);
56
+ }
57
+ export function zoomTransform(z) {
58
+ return `translate(${z.x}px, ${z.y}px) scale(${z.scale})`;
59
+ }
60
+ /** `m:ss.cc` — sub-second precision because frame-stepping is the point. */
61
+ export function formatMediaTime(sec) {
62
+ if (!Number.isFinite(sec) || sec < 0)
63
+ sec = 0;
64
+ const m = Math.floor(sec / 60);
65
+ const s = sec - m * 60;
66
+ return `${m}:${s.toFixed(2).padStart(5, '0')}`;
67
+ }
68
+ /** Move `frames` frames from `time`, clamped to [0, duration]. */
69
+ export function stepFrameTime(time, frames, duration, fps = DEFAULT_FPS) {
70
+ const rate = Number.isFinite(fps) && fps > 0 ? fps : DEFAULT_FPS;
71
+ const max = Number.isFinite(duration) && duration > 0 ? duration : Infinity;
72
+ const base = Number.isFinite(time) ? time : 0;
73
+ return Math.min(max, Math.max(0, base + frames / rate));
74
+ }
75
+ export function clampIndex(i, length) {
76
+ if (length <= 0 || !Number.isFinite(i))
77
+ return 0;
78
+ return Math.min(Math.max(0, Math.trunc(i)), length - 1);
79
+ }
80
+ export function stepView(index, delta, length, opts = {}) {
81
+ const next = index + delta;
82
+ if (next >= 0 && next < length)
83
+ return { kind: 'view', index: next };
84
+ if (delta > 0 && opts.hasNextItem)
85
+ return { kind: 'item', dir: 1 };
86
+ if (delta < 0 && opts.hasPrevItem)
87
+ return { kind: 'item', dir: -1 };
88
+ if (length < 2)
89
+ return { kind: 'none' };
90
+ return { kind: 'view', index: (next + length) % length };
91
+ }
92
+ /**
93
+ * j/k and ↓/↑ page ITEMS (the review-wide "next / previous item" keys), ←/→
94
+ * page VIEWS and spill into items at an edge, 1–9 pick a view. Verdict keys
95
+ * (a/x/r/b/u/n), rounds ([ / ]) and `?` are deliberately absent: they bubble
96
+ * out to the host, which owns them (the footer snippet renders VerdictControl).
97
+ * `k` is NOT play/pause here (it was in the MOS port) — it is "previous item".
98
+ */
99
+ export const MEDIA_ZOOM_KEYS = [
100
+ { action: 'play-pause', keys: [' '], label: 'Space', description: 'play/pause', video: true },
101
+ { action: 'frame-back', keys: [','], label: ',', description: 'frame back', video: true },
102
+ { action: 'frame-forward', keys: ['.'], label: '.', description: 'frame forward', video: true },
103
+ { action: 'slower', keys: ['<'], label: '<', description: 'slower', video: true },
104
+ { action: 'faster', keys: ['>'], label: '>', description: 'faster', video: true },
105
+ { action: 'loop', keys: ['l', 'L'], label: 'L', description: 'loop', video: true },
106
+ { action: 'zoom-in', keys: ['+', '='], label: '+', description: 'zoom in' },
107
+ { action: 'zoom-out', keys: ['-', '_'], label: '−', description: 'zoom out' },
108
+ { action: 'zoom-reset', keys: ['0'], label: '0', description: 'fit' },
109
+ { action: 'prev-view', keys: ['ArrowLeft'], label: '←', description: 'previous view' },
110
+ { action: 'next-view', keys: ['ArrowRight'], label: '→', description: 'next view' },
111
+ { action: 'pick-view', keys: ['1', '2', '3', '4', '5', '6', '7', '8', '9'], label: '1–9', description: 'pick view', views: true },
112
+ { action: 'next-item', keys: ['j', 'J', 'ArrowDown'], label: 'j', description: 'next item', items: true },
113
+ { action: 'prev-item', keys: ['k', 'K', 'ArrowUp'], label: 'k', description: 'previous item', items: true },
114
+ { action: 'close', keys: ['Escape'], label: 'Esc', description: 'close' }
115
+ ];
116
+ function applies(def, ctx) {
117
+ if (def.video && !ctx.isVideo)
118
+ return false;
119
+ if (def.items && !ctx.hasItems)
120
+ return false;
121
+ if (def.views && ctx.viewCount < 2)
122
+ return false;
123
+ return true;
124
+ }
125
+ /** keydown `key` → the action it triggers in this context, or null (let it bubble). */
126
+ export function resolveMediaZoomKey(key, ctx) {
127
+ for (const def of MEDIA_ZOOM_KEYS) {
128
+ if (def.keys.includes(key) && applies(def, ctx))
129
+ return def.action;
130
+ }
131
+ return null;
132
+ }
133
+ /** The keys that work right now, for the on-screen hint. */
134
+ export function mediaZoomKeys(ctx) {
135
+ return MEDIA_ZOOM_KEYS.filter((def) => {
136
+ if (!applies(def, ctx))
137
+ return false;
138
+ // ←/→ with a single view and no item paging do nothing.
139
+ if ((def.action === 'prev-view' || def.action === 'next-view') && ctx.viewCount < 2 && !ctx.hasItems)
140
+ return false;
141
+ return true;
142
+ });
143
+ }
144
+ /** One-line hint built from the key table: "Space play/pause · , frame back · …". */
145
+ export function mediaZoomHint(ctx) {
146
+ const parts = mediaZoomKeys(ctx).map((d) => `${d.label} ${d.description}`);
147
+ parts.push('wheel/pinch zoom, drag to pan');
148
+ return parts.join(' · ');
149
+ }
150
+ /** The next/previous entry in `speeds` from `current` (clamped at the ends). */
151
+ export function stepSpeed(current, dir, speeds = DEFAULT_SPEEDS) {
152
+ if (speeds.length === 0)
153
+ return current;
154
+ const sorted = [...speeds].sort((a, b) => a - b);
155
+ if (dir > 0)
156
+ return sorted.find((s) => s > current + 1e-9) ?? sorted[sorted.length - 1];
157
+ for (let i = sorted.length - 1; i >= 0; i--)
158
+ if (sorted[i] < current - 1e-9)
159
+ return sorted[i];
160
+ return sorted[0];
161
+ }
@@ -0,0 +1,101 @@
1
+ /**
2
+ * reviewSummary — the pure half of RoundHistory / ReviewProgress /
3
+ * ReviewFilterBar (jatui-xmrod, design mos-z8vgx docs/review-ux/proposal.md).
4
+ *
5
+ * No DOM, no runes: counting, the progress sentence, the standard filter set
6
+ * and round ordering live here so a host can compute the same numbers the
7
+ * components render (and tests can pin them without mounting anything).
8
+ *
9
+ * Verdicts are GENERIC: every block passes its own `{id, label, tone}` list
10
+ * (Pass/Fail/Blocked for a runbook, Approve/Needs work/Reject for a gallery).
11
+ * Nothing here knows any verdict id.
12
+ */
13
+ /** Semantic colour of a verdict. Maps to the theme's status colours. */
14
+ export type ReviewTone = 'success' | 'warning' | 'error' | 'info' | 'primary' | 'neutral';
15
+ /** One verdict a block offers. Shape-compatible with VerdictControl options. */
16
+ export interface ReviewVerdictDef {
17
+ id: string;
18
+ label: string;
19
+ tone: ReviewTone;
20
+ }
21
+ /** Counts of decided items keyed by verdict id. Unknown ids are ignored. */
22
+ export type ReviewCounts = Record<string, number>;
23
+ /** One locked, read-only line of an item's history. */
24
+ export interface ReviewRound {
25
+ /** Round key — a version id ("v7") or a retestSince timestamp. Unique per item. */
26
+ round: string;
27
+ /** Verdict id given in that round, or null/undefined when it closed undecided. */
28
+ verdict?: string | null;
29
+ note?: string | null;
30
+ /** When the verdict was given — Date, ISO string or epoch ms. */
31
+ at?: Date | string | number | null;
32
+ /** Display label override ("v7"); defaults to "Round N". */
33
+ label?: string;
34
+ }
35
+ /** One chip of a ReviewFilterBar. */
36
+ export interface ReviewFilterChip {
37
+ id: string;
38
+ label: string;
39
+ count: number;
40
+ tone?: ReviewTone;
41
+ }
42
+ export interface ReviewSummary {
43
+ total: number;
44
+ /** Items with a known verdict (clamped to total). */
45
+ decided: number;
46
+ undecided: number;
47
+ retest: number;
48
+ /** Per-verdict segments in `verdicts` order, zero counts dropped. */
49
+ segments: {
50
+ id: string;
51
+ label: string;
52
+ tone: ReviewTone;
53
+ count: number;
54
+ }[];
55
+ }
56
+ /**
57
+ * Sum per-block counts into one page-level count — the "aggregated
58
+ * ReviewProgress" in the Finish bar. Pass any number of count maps.
59
+ */
60
+ export declare function mergeReviewCounts(...parts: (ReviewCounts | null | undefined)[]): ReviewCounts;
61
+ /**
62
+ * Normalise counts against the verdict list and the item total. Decided is
63
+ * clamped to `total`, so a stale count can never draw a bar past 100%.
64
+ */
65
+ export declare function summarizeReview(verdicts: readonly ReviewVerdictDef[], counts: ReviewCounts, total: number, retest?: number): ReviewSummary;
66
+ /**
67
+ * "12 of 19 decided · 9 pass · 2 fail · 1 blocked · 4 to retest".
68
+ * Zero-count parts are omitted; verdict labels are lower-cased.
69
+ */
70
+ export declare function reviewSentence(summary: ReviewSummary, decidedWord?: string): string;
71
+ export interface BuildFiltersInput {
72
+ verdicts: readonly ReviewVerdictDef[];
73
+ counts: ReviewCounts;
74
+ total: number;
75
+ /** Items waiting on this viewer (retest first, then undecided). Omit to skip the chip. */
76
+ needsYou?: number;
77
+ /** Items in an open retest round. Omit to skip the chip. */
78
+ retest?: number;
79
+ /** Drop chips whose count is 0 (All is always kept). Default true. */
80
+ hideEmpty?: boolean;
81
+ }
82
+ /**
83
+ * The standard chip set, in the design's order:
84
+ * All · Needs you · Undecided · one per verdict · Retest.
85
+ * Ids are `all`, `needs-you`, `undecided`, the verdict id, `retest`.
86
+ */
87
+ export declare function buildReviewFilters(input: BuildFiltersInput): ReviewFilterChip[];
88
+ /**
89
+ * Rounds newest first, each paired with its 1-based chronological number.
90
+ * When every round has a parseable `at` they are ordered by it; otherwise the
91
+ * input is taken as append order (oldest first — how `history[]` grows).
92
+ */
93
+ export declare function orderRounds(rounds: readonly ReviewRound[]): {
94
+ entry: ReviewRound;
95
+ n: number;
96
+ }[];
97
+ /** "Oct 10" (same year) / "Oct 10, 2025" — empty for a missing/invalid date. */
98
+ export declare function formatRoundDate(at: ReviewRound['at'], now?: Date): string;
99
+ /** DaisyUI status-colour background per tone — kept here so all three components agree. */
100
+ export declare const TONE_BG: Record<ReviewTone, string>;
101
+ export declare const TONE_TEXT: Record<ReviewTone, string>;
@@ -0,0 +1,122 @@
1
+ /**
2
+ * reviewSummary — the pure half of RoundHistory / ReviewProgress /
3
+ * ReviewFilterBar (jatui-xmrod, design mos-z8vgx docs/review-ux/proposal.md).
4
+ *
5
+ * No DOM, no runes: counting, the progress sentence, the standard filter set
6
+ * and round ordering live here so a host can compute the same numbers the
7
+ * components render (and tests can pin them without mounting anything).
8
+ *
9
+ * Verdicts are GENERIC: every block passes its own `{id, label, tone}` list
10
+ * (Pass/Fail/Blocked for a runbook, Approve/Needs work/Reject for a gallery).
11
+ * Nothing here knows any verdict id.
12
+ */
13
+ const nonNeg = (n) => (typeof n === 'number' && Number.isFinite(n) && n > 0 ? Math.floor(n) : 0);
14
+ /**
15
+ * Sum per-block counts into one page-level count — the "aggregated
16
+ * ReviewProgress" in the Finish bar. Pass any number of count maps.
17
+ */
18
+ export function mergeReviewCounts(...parts) {
19
+ const out = {};
20
+ for (const part of parts) {
21
+ if (!part)
22
+ continue;
23
+ for (const [id, n] of Object.entries(part)) {
24
+ const v = nonNeg(n);
25
+ if (v)
26
+ out[id] = (out[id] ?? 0) + v;
27
+ }
28
+ }
29
+ return out;
30
+ }
31
+ /**
32
+ * Normalise counts against the verdict list and the item total. Decided is
33
+ * clamped to `total`, so a stale count can never draw a bar past 100%.
34
+ */
35
+ export function summarizeReview(verdicts, counts, total, retest = 0) {
36
+ const t = nonNeg(total);
37
+ let remaining = t;
38
+ const segments = [];
39
+ for (const v of verdicts) {
40
+ const n = Math.min(nonNeg(counts[v.id]), remaining);
41
+ if (!n)
42
+ continue;
43
+ remaining -= n;
44
+ segments.push({ id: v.id, label: v.label, tone: v.tone, count: n });
45
+ }
46
+ const decided = t - remaining;
47
+ return { total: t, decided, undecided: remaining, retest: Math.min(nonNeg(retest), t), segments };
48
+ }
49
+ /**
50
+ * "12 of 19 decided · 9 pass · 2 fail · 1 blocked · 4 to retest".
51
+ * Zero-count parts are omitted; verdict labels are lower-cased.
52
+ */
53
+ export function reviewSentence(summary, decidedWord = 'decided') {
54
+ const parts = [`${summary.decided} of ${summary.total} ${decidedWord}`];
55
+ for (const s of summary.segments)
56
+ parts.push(`${s.count} ${s.label.toLowerCase()}`);
57
+ if (summary.retest)
58
+ parts.push(`${summary.retest} to retest`);
59
+ return parts.join(' · ');
60
+ }
61
+ /**
62
+ * The standard chip set, in the design's order:
63
+ * All · Needs you · Undecided · one per verdict · Retest.
64
+ * Ids are `all`, `needs-you`, `undecided`, the verdict id, `retest`.
65
+ */
66
+ export function buildReviewFilters(input) {
67
+ const { verdicts, counts, total, needsYou, retest, hideEmpty = true } = input;
68
+ const sum = summarizeReview(verdicts, counts, total);
69
+ const chips = [{ id: 'all', label: 'All', count: sum.total }];
70
+ if (needsYou != null)
71
+ chips.push({ id: 'needs-you', label: 'Needs you', count: nonNeg(needsYou) });
72
+ chips.push({ id: 'undecided', label: 'Undecided', count: sum.undecided });
73
+ for (const v of verdicts)
74
+ chips.push({ id: v.id, label: v.label, tone: v.tone, count: nonNeg(counts[v.id]) });
75
+ if (retest != null)
76
+ chips.push({ id: 'retest', label: 'Retest', count: nonNeg(retest) });
77
+ return hideEmpty ? chips.filter((c) => c.id === 'all' || c.count > 0) : chips;
78
+ }
79
+ const toMs = (at) => {
80
+ if (at == null)
81
+ return NaN;
82
+ return at instanceof Date ? at.getTime() : new Date(at).getTime();
83
+ };
84
+ /**
85
+ * Rounds newest first, each paired with its 1-based chronological number.
86
+ * When every round has a parseable `at` they are ordered by it; otherwise the
87
+ * input is taken as append order (oldest first — how `history[]` grows).
88
+ */
89
+ export function orderRounds(rounds) {
90
+ const indexed = rounds.map((entry, i) => ({ entry, i, ms: toMs(entry.at) }));
91
+ const allTimed = indexed.length > 0 && indexed.every((r) => Number.isFinite(r.ms));
92
+ const chrono = allTimed ? [...indexed].sort((a, b) => a.ms - b.ms || a.i - b.i) : indexed;
93
+ return chrono.map((r, k) => ({ entry: r.entry, n: k + 1 })).reverse();
94
+ }
95
+ /** "Oct 10" (same year) / "Oct 10, 2025" — empty for a missing/invalid date. */
96
+ export function formatRoundDate(at, now = new Date()) {
97
+ const ms = toMs(at);
98
+ if (!Number.isFinite(ms))
99
+ return '';
100
+ const d = new Date(ms);
101
+ const opts = { month: 'short', day: 'numeric' };
102
+ if (d.getFullYear() !== now.getFullYear())
103
+ opts.year = 'numeric';
104
+ return d.toLocaleDateString(undefined, opts);
105
+ }
106
+ /** DaisyUI status-colour background per tone — kept here so all three components agree. */
107
+ export const TONE_BG = {
108
+ success: 'bg-success',
109
+ warning: 'bg-warning',
110
+ error: 'bg-error',
111
+ info: 'bg-info',
112
+ primary: 'bg-primary',
113
+ neutral: 'bg-neutral'
114
+ };
115
+ export const TONE_TEXT = {
116
+ success: 'text-success',
117
+ warning: 'text-warning',
118
+ error: 'text-error',
119
+ info: 'text-info',
120
+ primary: 'text-primary',
121
+ neutral: 'text-base-content/70'
122
+ };
package/dist/index.d.ts CHANGED
@@ -284,3 +284,17 @@ export { default as BottomSheet } from './components/BottomSheet.svelte';
284
284
  export { default as VariantMatrixPicker } from './components/variant/VariantMatrixPicker.svelte';
285
285
  export { countCombinations, generateCombinations, generateVariantFilename, findVariantOption, variantCategoryMode } from './components/variant/variantCombinations';
286
286
  export type { VariantCategory, VariantOption, VariantSelection, VariantSelectionMode, VariantCombination } from './components/variant/variantCombinations';
287
+ export { default as VerdictControl } from './components/review/VerdictControl.svelte';
288
+ export { default as ReviewNote, NOTE_HINT } from './components/review/ReviewNote.svelte';
289
+ export type { ReviewNoteSaveState } from './components/review/ReviewNote.svelte';
290
+ export { default as ReviewShortcuts } from './components/review/ReviewShortcuts.svelte';
291
+ export { default as RoundHistory } from './components/review/RoundHistory.svelte';
292
+ export { default as ReviewProgress } from './components/review/ReviewProgress.svelte';
293
+ export { default as ReviewFilterBar } from './components/review/ReviewFilterBar.svelte';
294
+ export { default as MediaZoomLightbox } from './components/review/MediaZoomLightbox.svelte';
295
+ export { reviewKeys, createReviewKeys, reviewKeyRows, resolveReviewKey, REVIEW_KEY_TABLE } from './actions/reviewKeys';
296
+ export type { VerdictTone, VerdictOption, NeedsYou, ReviewKeyAction, ReviewKeyEntry, ReviewKeysOptions, ReviewKeyRow, ReviewKeysController } from './actions/reviewKeys';
297
+ export { mergeReviewCounts, summarizeReview, reviewSentence, buildReviewFilters, orderRounds, formatRoundDate } from './components/review/reviewSummary';
298
+ export type { ReviewTone, ReviewVerdictDef, ReviewCounts, ReviewRound, ReviewFilterChip, ReviewSummary, BuildFiltersInput } from './components/review/reviewSummary';
299
+ export { MEDIA_ZOOM_KEYS, mediaZoomHint } from './components/review/mediaZoom';
300
+ export type { MediaZoomState, MediaZoomView, MediaZoomFooterContext, MediaZoomAction, MediaZoomKeyDef, MediaZoomKeyContext } from './components/review/mediaZoom';
package/dist/index.js CHANGED
@@ -336,3 +336,15 @@ export { default as BottomSheet } from './components/BottomSheet.svelte';
336
336
  // Variant matrix picker + combination algebra (harvested from vid-gen FrameVariantGenerator — jatui-m4uz3.8).
337
337
  export { default as VariantMatrixPicker } from './components/variant/VariantMatrixPicker.svelte';
338
338
  export { countCombinations, generateCombinations, generateVariantFilename, findVariantOption, variantCategoryMode } from './components/variant/variantCombinations';
339
+ // Review primitives (MOS epic mos-lsmve — jatui-et7pm/xmrod/c79nt, exported by jatui-2h5v9).
340
+ // Full surface (incl. zoom/pan math and tone class maps) on the 'jatui/review' subpath.
341
+ export { default as VerdictControl } from './components/review/VerdictControl.svelte';
342
+ export { default as ReviewNote, NOTE_HINT } from './components/review/ReviewNote.svelte';
343
+ export { default as ReviewShortcuts } from './components/review/ReviewShortcuts.svelte';
344
+ export { default as RoundHistory } from './components/review/RoundHistory.svelte';
345
+ export { default as ReviewProgress } from './components/review/ReviewProgress.svelte';
346
+ export { default as ReviewFilterBar } from './components/review/ReviewFilterBar.svelte';
347
+ export { default as MediaZoomLightbox } from './components/review/MediaZoomLightbox.svelte';
348
+ export { reviewKeys, createReviewKeys, reviewKeyRows, resolveReviewKey, REVIEW_KEY_TABLE } from './actions/reviewKeys';
349
+ export { mergeReviewCounts, summarizeReview, reviewSentence, buildReviewFilters, orderRounds, formatRoundDate } from './components/review/reviewSummary';
350
+ export { MEDIA_ZOOM_KEYS, mediaZoomHint } from './components/review/mediaZoom';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@joewinke/jatui",
3
- "version": "0.32.0",
3
+ "version": "0.34.0",
4
4
  "private": false,
5
5
  "description": "Shared Svelte 5 component library for JAT projects",
6
6
  "type": "module",
@@ -68,6 +68,11 @@
68
68
  "types": "./dist/meet/index.d.ts",
69
69
  "svelte": "./dist/meet/index.js",
70
70
  "default": "./dist/meet/index.js"
71
+ },
72
+ "./review": {
73
+ "types": "./dist/components/review/index.d.ts",
74
+ "svelte": "./dist/components/review/index.js",
75
+ "default": "./dist/components/review/index.js"
71
76
  }
72
77
  },
73
78
  "files": [