@rslsp1/fa-app-tools 4.0.0 → 5.0.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/dist/szenen.js CHANGED
@@ -146,13 +146,16 @@ __export(szenen_exports, {
146
146
  freezeDraftChain: () => freezeDraftChain,
147
147
  freezeSceneWorkingState: () => freezeSceneWorkingState,
148
148
  getSzenenSchemas: () => getSzenenSchemas,
149
+ gruppiereZuItems: () => gruppiereZuItems,
149
150
  hasPlacementCapability: () => hasPlacementCapability,
150
151
  isKnownElementType: () => isKnownElementType,
151
152
  isOk: () => isOk,
152
153
  isProjectionCurrent: () => isProjectionCurrent,
153
154
  isSzenenItemId: () => isSzenenItemId,
154
155
  itemIdForRun: () => itemIdForRun,
156
+ ladeVorschauen: () => ladeVorschauen,
155
157
  linkFor: () => linkFor,
158
+ locatorTeile: () => locatorTeile,
156
159
  logRevisionOf: () => logRevisionOf,
157
160
  mergeSzenenBatchOrigin: () => mergeSzenenBatchOrigin,
158
161
  noticeFor: () => noticeFor,
@@ -10200,6 +10203,7 @@ function createHfImageStore(options) {
10200
10203
  const basisMs = options.leseBasisMs ?? 400;
10201
10204
  const schlafen2 = options.schlafen ?? warten;
10202
10205
  const abgelegt = /* @__PURE__ */ new Map();
10206
+ const vorschauen = /* @__PURE__ */ new Map();
10203
10207
  const bekanntePfade = /* @__PURE__ */ new Set();
10204
10208
  let gefalteterBestand = null;
10205
10209
  let laufenderAbgleich = null;
@@ -10223,7 +10227,7 @@ function createHfImageStore(options) {
10223
10227
  }
10224
10228
  return laufenderAbgleich;
10225
10229
  }
10226
- return {
10230
+ const speicher = {
10227
10231
  bestand: bestandAbgleichen,
10228
10232
  async putWithId(id, base64, mimeType, titel) {
10229
10233
  const echterTyp = typAusBytes(base64, mimeType);
@@ -10242,6 +10246,38 @@ function createHfImageStore(options) {
10242
10246
  }, []);
10243
10247
  abgelegt.set(id, { data: base64, mimeType: echterTyp });
10244
10248
  },
10249
+ async preview(id) {
10250
+ const bekannt = vorschauen.get(id);
10251
+ if (bekannt) return bekannt;
10252
+ const eben = abgelegt.get(id);
10253
+ if (eben) {
10254
+ const quelle2 = `data:${eben.mimeType};base64,${eben.data}`;
10255
+ vorschauen.set(id, quelle2);
10256
+ return quelle2;
10257
+ }
10258
+ const zustand = await bestandAbgleichen();
10259
+ const eintrag = zustand.metadata.find((e) => e.id === id);
10260
+ if (eintrag?.hasThumb && eintrag.filename) {
10261
+ const klein = await hfLoadImageAsBase64(
10262
+ id,
10263
+ token,
10264
+ void 0,
10265
+ eintrag.filename,
10266
+ void 0,
10267
+ eintrag.mimeType,
10268
+ true
10269
+ );
10270
+ if (klein) {
10271
+ const quelle2 = `data:${eintrag.mimeType || "image/jpeg"};base64,${klein}`;
10272
+ vorschauen.set(id, quelle2);
10273
+ return quelle2;
10274
+ }
10275
+ }
10276
+ const voll = await speicher.resolve(id);
10277
+ const quelle = `data:${voll.mimeType};base64,${voll.data}`;
10278
+ vorschauen.set(id, quelle);
10279
+ return quelle;
10280
+ },
10245
10281
  async resolve(id, mimeType) {
10246
10282
  const eben = abgelegt.get(id);
10247
10283
  if (eben) return eben;
@@ -10279,6 +10315,47 @@ function createHfImageStore(options) {
10279
10315
  );
10280
10316
  }
10281
10317
  };
10318
+ return speicher;
10319
+ }
10320
+
10321
+ // src/lib/szenen/libraryItems.ts
10322
+ var LOCATOR = /^(.+)-(\d+)$/;
10323
+ function locatorTeile(mediaRef) {
10324
+ const treffer = LOCATOR.exec(mediaRef);
10325
+ if (!treffer) return { stamm: mediaRef, position: null };
10326
+ return { stamm: treffer[1], position: Number(treffer[2]) };
10327
+ }
10328
+ function gruppiereZuItems(bilder) {
10329
+ const gruppen = /* @__PURE__ */ new Map();
10330
+ for (const bild of bilder) {
10331
+ const { stamm } = locatorTeile(bild.mediaRef);
10332
+ const vorhanden = gruppen.get(stamm);
10333
+ if (vorhanden) vorhanden.push(bild);
10334
+ else gruppen.set(stamm, [bild]);
10335
+ }
10336
+ const items = [...gruppen.entries()].map(([stamm, mitglieder]) => {
10337
+ const sortiert = [...mitglieder].sort((a, b) => {
10338
+ const pa = locatorTeile(a.mediaRef).position ?? 0;
10339
+ const pb = locatorTeile(b.mediaRef).position ?? 0;
10340
+ return pa - pb;
10341
+ });
10342
+ const erstes = sortiert[0];
10343
+ const itemRef = sortiert.length === 1 ? erstes.mediaRef : stamm;
10344
+ const juengstes = sortiert.reduce(
10345
+ (max, b) => Math.max(max, b.timestamp ?? 0),
10346
+ 0
10347
+ );
10348
+ return {
10349
+ item: {
10350
+ itemRef,
10351
+ title: erstes.itemTitle?.trim() || erstes.title || itemRef,
10352
+ images: sortiert.map(({ timestamp: _t, itemTitle: _i, ...bild }) => bild)
10353
+ },
10354
+ juengstes
10355
+ };
10356
+ });
10357
+ items.sort((a, b) => b.juengstes - a.juengstes);
10358
+ return items.map((e) => e.item);
10282
10359
  }
10283
10360
 
10284
10361
  // src/lib/szenen/hfImageLibrary.ts
@@ -10286,13 +10363,22 @@ function fehler(nachricht, retryable = false) {
10286
10363
  return { kind: "error", message: nachricht, retryable };
10287
10364
  }
10288
10365
  function alsBild(eintrag) {
10366
+ const { position } = locatorTeile(eintrag.id);
10367
+ const werk = eintrag.prompt?.trim() || "";
10289
10368
  return {
10290
10369
  mediaRef: eintrag.id,
10291
- // Der Prompt ist der einzige menschenlesbare Text, den ein Bild dort trägt.
10292
- // Fehlt er, ist die Kennung ehrlicher als ein erfundener Name.
10293
- title: eintrag.prompt?.trim() || eintrag.id,
10370
+ // Innerhalb eines mehrbildigen Werkes tragen alle Einträge denselben
10371
+ // Prompt — der Abgleich schreibt ihn je Bild aus derselben Library-Zeile.
10372
+ // Deshalb bekommt die Kachel dort die Position dazu, sonst stünde
10373
+ // vierzehnmal derselbe Satz.
10374
+ title: position === null ? werk || eintrag.id : `${werk || eintrag.id} \xB7 Bild ${position + 1}`,
10294
10375
  mimeType: eintrag.mimeType || "image/jpeg",
10295
- thumbUrl: null
10376
+ // Kein `thumbUrl`: Die Datei liegt auf HF hinter einem Token, und ein
10377
+ // `<img src>` käme dort nicht durch. Die Quelle entsteht über
10378
+ // `loadPreview()` — und nur für die Kacheln, die wirklich gezeigt werden.
10379
+ thumbUrl: null,
10380
+ itemTitle: werk || null,
10381
+ ...typeof eintrag.timestamp === "number" ? { timestamp: eintrag.timestamp } : {}
10296
10382
  };
10297
10383
  }
10298
10384
  function createHfImageLibrary(options) {
@@ -10303,15 +10389,14 @@ function createHfImageLibrary(options) {
10303
10389
  ...options.now ? { now: options.now } : {}
10304
10390
  });
10305
10391
  return {
10306
- async listImages(query) {
10392
+ async listItems(query) {
10307
10393
  try {
10308
10394
  const zustand = await store.bestand();
10309
10395
  const eintraege = zustand?.metadata ?? [];
10396
+ const items = gruppiereZuItems(eintraege.map(alsBild));
10310
10397
  const gesucht = query?.search?.trim().toLowerCase();
10311
- const gefiltert = gesucht ? eintraege.filter((e) => (e.prompt ?? "").toLowerCase().includes(gesucht) || e.id.toLowerCase().includes(gesucht)) : eintraege;
10312
- const sortiert = [...gefiltert].sort((a, b) => (b.timestamp ?? 0) - (a.timestamp ?? 0));
10313
- const begrenzt = query?.limit ? sortiert.slice(0, query.limit) : sortiert;
10314
- return { kind: "ok", value: begrenzt.map(alsBild) };
10398
+ const gefiltert = gesucht ? items.filter((i) => i.title.toLowerCase().includes(gesucht) || i.itemRef.toLowerCase().includes(gesucht)) : items;
10399
+ return { kind: "ok", value: query?.limit ? gefiltert.slice(0, query.limit) : gefiltert };
10315
10400
  } catch (error) {
10316
10401
  return fehler(
10317
10402
  `Der Bildbestand konnte nicht gelesen werden: ${error instanceof Error ? error.message : String(error)}`,
@@ -10319,6 +10404,16 @@ function createHfImageLibrary(options) {
10319
10404
  );
10320
10405
  }
10321
10406
  },
10407
+ async loadPreview(mediaRef) {
10408
+ try {
10409
+ return { kind: "ok", value: await store.preview(mediaRef) };
10410
+ } catch (error) {
10411
+ return fehler(
10412
+ `Die Vorschau von ${mediaRef} ist nicht ladbar: ${error instanceof Error ? error.message : String(error)}`,
10413
+ true
10414
+ );
10415
+ }
10416
+ },
10322
10417
  async resolveImage(mediaRef, mimeTypeHinweis) {
10323
10418
  try {
10324
10419
  const bild = await store.resolve(mediaRef, mimeTypeHinweis);
@@ -10331,10 +10426,13 @@ function createHfImageLibrary(options) {
10331
10426
  const id = newImageId();
10332
10427
  try {
10333
10428
  await store.putWithId(id, input.data, input.mime_type, input.title);
10334
- return {
10335
- kind: "ok",
10336
- value: { mediaRef: id, title: input.title, mimeType: input.mime_type, thumbUrl: null }
10429
+ const abgelegt = {
10430
+ mediaRef: id,
10431
+ title: input.title,
10432
+ mimeType: input.mime_type,
10433
+ thumbUrl: null
10337
10434
  };
10435
+ return { kind: "ok", value: abgelegt };
10338
10436
  } catch (error) {
10339
10437
  return fehler(
10340
10438
  `Das Bild konnte nicht abgelegt werden: ${error instanceof Error ? error.message : String(error)}`,
@@ -10345,6 +10443,29 @@ function createHfImageLibrary(options) {
10345
10443
  };
10346
10444
  }
10347
10445
 
10446
+ // src/lib/szenen/bildVorschau.ts
10447
+ async function ladeVorschauen(port, mediaRefs, fertig, optionen = {}) {
10448
+ const laden = port.loadPreview;
10449
+ if (!laden) return;
10450
+ const parallel = Math.max(1, optionen.parallel ?? 4);
10451
+ const lauf = optionen.lauf;
10452
+ const offen = mediaRefs.filter((ref) => !(optionen.hat?.(ref) ?? false));
10453
+ let naechster = 0;
10454
+ async function arbeiter() {
10455
+ while (naechster < offen.length) {
10456
+ if (lauf?.abgebrochen) return;
10457
+ const ref = offen[naechster];
10458
+ naechster += 1;
10459
+ const ergebnis = await laden.call(port, ref);
10460
+ if (lauf?.abgebrochen) return;
10461
+ if (ergebnis.kind === "ok") fertig(ref, ergebnis.value);
10462
+ }
10463
+ }
10464
+ await Promise.all(
10465
+ Array.from({ length: Math.min(parallel, offen.length) }, () => arbeiter())
10466
+ );
10467
+ }
10468
+
10348
10469
  // src/lib/szenen/imageGenerationCapability.ts
10349
10470
  var isObject11 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
10350
10471
  var istText5 = (v) => typeof v === "string" && v.trim().length > 0;
@@ -11032,15 +11153,26 @@ function createMemoryViewPort(options = {}) {
11032
11153
  }
11033
11154
  };
11034
11155
  const bilder = /* @__PURE__ */ new Map();
11156
+ const werke = [];
11035
11157
  for (const bild of options.images ?? []) {
11036
11158
  bilder.set(bild.mediaRef, { bild, data: "AAAA" });
11159
+ werke.push({ itemRef: bild.mediaRef, title: bild.title, images: [bild] });
11037
11160
  }
11161
+ for (const werk of options.imageItems ?? []) {
11162
+ for (const bild of werk.images) bilder.set(bild.mediaRef, { bild, data: "AAAA" });
11163
+ werke.push(werk);
11164
+ }
11165
+ const vorschauLazy = options.imagePreviewLazy === true;
11038
11166
  const imageLibraryPort = {
11039
- async listImages(query) {
11040
- const alle = [...bilder.values()].map((e) => e.bild);
11167
+ async listItems(query) {
11041
11168
  const gesucht = query?.search?.trim().toLowerCase();
11042
- const gefiltert = gesucht ? alle.filter((b) => b.title.toLowerCase().includes(gesucht)) : alle;
11043
- return ok2(query?.limit ? gefiltert.slice(0, query.limit) : gefiltert);
11169
+ const gefiltert = gesucht ? werke.filter((w) => w.title.toLowerCase().includes(gesucht)) : werke;
11170
+ const sichtbar = query?.limit ? gefiltert.slice(0, query.limit) : gefiltert;
11171
+ if (!vorschauLazy) return ok2(sichtbar);
11172
+ return ok2(sichtbar.map((w) => ({
11173
+ ...w,
11174
+ images: w.images.map((b) => ({ ...b, thumbUrl: null }))
11175
+ })));
11044
11176
  },
11045
11177
  async resolveImage(mediaRef) {
11046
11178
  const eintrag = bilder.get(mediaRef);
@@ -11058,9 +11190,19 @@ function createMemoryViewPort(options = {}) {
11058
11190
  thumbUrl: null
11059
11191
  };
11060
11192
  bilder.set(mediaRef, { bild, data: input.data });
11193
+ werke.unshift({ itemRef: mediaRef, title: bild.title, images: [bild] });
11061
11194
  return done(bild);
11062
11195
  }
11063
11196
  };
11197
+ if (vorschauLazy) {
11198
+ imageLibraryPort.loadPreview = async (mediaRef) => {
11199
+ const eintrag = bilder.get(mediaRef);
11200
+ if (!eintrag) {
11201
+ return { kind: "error", message: `Bild ${mediaRef} ist unbekannt.`, retryable: false };
11202
+ }
11203
+ return ok2(`data:${eintrag.bild.mimeType};base64,${eintrag.data}`);
11204
+ };
11205
+ }
11064
11206
  const batches = [...options.batches ?? []];
11065
11207
  const imageGenerationEvents = createInMemoryEventChannel();
11066
11208
  const imageBatchHistoryPort = {
@@ -17546,14 +17688,64 @@ function readAsBase642(datei) {
17546
17688
  leser.readAsDataURL(datei);
17547
17689
  });
17548
17690
  }
17691
+ function useVorschauen(port, mediaRefs) {
17692
+ const [quellen, setQuellen] = (0, import_react15.useState)(() => /* @__PURE__ */ new Map());
17693
+ const gehalten = (0, import_react15.useRef)(/* @__PURE__ */ new Map());
17694
+ const schluessel = mediaRefs.join(" ");
17695
+ (0, import_react15.useEffect)(() => {
17696
+ const bestand = port.imageLibrary;
17697
+ if (!bestand?.loadPreview) return void 0;
17698
+ const refs = schluessel ? schluessel.split(" ") : [];
17699
+ const lauf = { abgebrochen: false };
17700
+ void ladeVorschauen(
17701
+ bestand,
17702
+ refs,
17703
+ (mediaRef, quelle) => {
17704
+ gehalten.current.set(mediaRef, quelle);
17705
+ setQuellen(new Map(gehalten.current));
17706
+ },
17707
+ { parallel: 4, hat: (ref) => gehalten.current.has(ref), lauf }
17708
+ );
17709
+ return () => {
17710
+ lauf.abgebrochen = true;
17711
+ };
17712
+ }, [port, schluessel]);
17713
+ return quellen;
17714
+ }
17715
+ var Kachel = ({ quelle, alt }) => /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("span", { className: "szenen-bildkachel", children: quelle ? /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("img", { src: quelle, alt, loading: "lazy" }) : /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("span", { className: "szenen-bildkachel__leer", "aria-hidden": "true" }) });
17549
17716
  var BildWaehler = ({ port, gewaehlt, onWaehlen, onEntfernen }) => {
17550
17717
  const [suche, setSuche] = (0, import_react15.useState)("");
17551
- const [bilder] = usePortQuery(
17552
- () => port.imageLibrary.listImages({ search: suche.trim() || void 0, limit: 60 }),
17718
+ const [offenesWerk, setOffenesWerk] = (0, import_react15.useState)(null);
17719
+ const [items] = usePortQuery(
17720
+ () => port.imageLibrary.listItems({ search: suche.trim() || void 0, limit: 60 }),
17553
17721
  [port, suche]
17554
17722
  );
17723
+ const werk = items.status === "ready" ? items.value.find((i) => i.itemRef === offenesWerk) ?? null : null;
17724
+ const sichtbar = (0, import_react15.useMemo)(() => {
17725
+ const refs = werk ? werk.images.map((b) => b.mediaRef) : items.status === "ready" ? items.value.map((i) => i.images[0]?.mediaRef).filter((r) => Boolean(r)) : [];
17726
+ return [.../* @__PURE__ */ new Set([...refs, ...gewaehlt.map((b) => b.mediaRef)])];
17727
+ }, [items, werk, gewaehlt]);
17728
+ const quellen = useVorschauen(port, sichtbar);
17729
+ const quelleVon = (bild) => bild.thumbUrl || quellen.get(bild.mediaRef) || null;
17555
17730
  return /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { className: "szenen-bildwahl", "data-testid": "bildwahl", children: [
17556
- /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("label", { className: "szenen-field", children: [
17731
+ werk ? /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { className: "szenen-bildwahl__kopf", children: [
17732
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
17733
+ "button",
17734
+ {
17735
+ type: "button",
17736
+ className: "szenen-bildwahl__zurueck",
17737
+ onClick: () => setOffenesWerk(null),
17738
+ "data-testid": "bildwahl-zurueck",
17739
+ children: "Zurueck zur Auswahl"
17740
+ }
17741
+ ),
17742
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("span", { className: "szenen-bildwahl__werktitel", "data-testid": "bildwahl-werktitel", children: [
17743
+ werk.title,
17744
+ " \xB7 ",
17745
+ werk.images.length,
17746
+ " Bilder"
17747
+ ] })
17748
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("label", { className: "szenen-field", children: [
17557
17749
  /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("span", { children: "Bild aus dem Bestand" }),
17558
17750
  /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
17559
17751
  "input",
@@ -17561,31 +17753,70 @@ var BildWaehler = ({ port, gewaehlt, onWaehlen, onEntfernen }) => {
17561
17753
  type: "search",
17562
17754
  value: suche,
17563
17755
  onChange: (e) => setSuche(e.target.value),
17756
+ onKeyDown: (e) => {
17757
+ if (e.key === "Enter") e.preventDefault();
17758
+ },
17564
17759
  placeholder: "Suchen \u2026",
17565
17760
  "data-testid": "bild-suche"
17566
17761
  }
17567
17762
  )
17568
17763
  ] }),
17569
- bilder.status === "loading" && /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("p", { children: "Bilder werden geladen \u2026" }),
17570
- bilder.status === "error" && /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("p", { className: "szenen-error", role: "alert", children: [
17764
+ items.status === "loading" && /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("p", { children: "Bilder werden geladen \u2026" }),
17765
+ items.status === "error" && /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("p", { className: "szenen-error", role: "alert", children: [
17571
17766
  "Der Bildbestand ist nicht lesbar: ",
17572
- bilder.message
17767
+ items.message
17573
17768
  ] }),
17574
- bilder.status === "ready" && bilder.value.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("p", { className: "szenen-empty", "data-testid": "bildwahl-leer", children: "Zu dieser Suche gibt es kein Bild. Ohne Treffer bleibt der Upload." }),
17575
- bilder.status === "ready" && bilder.value.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("ul", { className: "szenen-bildwahl__liste", children: bilder.value.map((bild) => /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(
17769
+ items.status === "ready" && items.value.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("p", { className: "szenen-empty", "data-testid": "bildwahl-leer", children: "Zu dieser Suche gibt es kein Bild. Ohne Treffer bleibt der Upload." }),
17770
+ werk && /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("ul", { className: "szenen-bildwahl__raster", "data-testid": "bildwahl-galerie", children: werk.images.map((bild, position) => /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(
17576
17771
  "button",
17577
17772
  {
17578
17773
  type: "button",
17579
- onClick: () => onWaehlen(bild),
17774
+ onClick: () => {
17775
+ onWaehlen(bild);
17776
+ setOffenesWerk(null);
17777
+ },
17580
17778
  "data-testid": `bild-waehlen-${bild.mediaRef}`,
17779
+ title: bild.title,
17581
17780
  children: [
17582
- bild.thumbUrl && /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("img", { src: bild.thumbUrl, alt: "" }),
17583
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("span", { children: bild.title })
17781
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(Kachel, { quelle: quelleVon(bild), alt: bild.title }),
17782
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("span", { className: "szenen-bildwahl__nummer", children: [
17783
+ "#",
17784
+ position + 1
17785
+ ] })
17584
17786
  ]
17585
17787
  }
17586
17788
  ) }, bild.mediaRef)) }),
17789
+ !werk && items.status === "ready" && items.value.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("ul", { className: "szenen-bildwahl__raster", "data-testid": "bildwahl-werke", children: items.value.map((item) => {
17790
+ const deckbild = item.images[0];
17791
+ if (!deckbild) return null;
17792
+ return /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(
17793
+ "button",
17794
+ {
17795
+ type: "button",
17796
+ onClick: () => item.images.length === 1 ? onWaehlen(deckbild) : setOffenesWerk(item.itemRef),
17797
+ "data-testid": `item-waehlen-${item.itemRef}`,
17798
+ title: item.title,
17799
+ children: [
17800
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(Kachel, { quelle: quelleVon(deckbild), alt: item.title }),
17801
+ item.images.length > 1 && /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(
17802
+ "span",
17803
+ {
17804
+ className: "szenen-bildwahl__anzahl",
17805
+ "data-testid": `item-anzahl-${item.itemRef}`,
17806
+ children: [
17807
+ item.images.length,
17808
+ "x"
17809
+ ]
17810
+ }
17811
+ ),
17812
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("span", { className: "szenen-bildwahl__titel", children: item.title })
17813
+ ]
17814
+ }
17815
+ ) }, item.itemRef);
17816
+ }) }),
17587
17817
  gewaehlt.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("ul", { className: "szenen-bildwahl__gewaehlt", "data-testid": "bilder-gewaehlt", children: gewaehlt.map((bild) => /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("li", { children: [
17588
- bild.title,
17818
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(Kachel, { quelle: quelleVon(bild), alt: bild.title }),
17819
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("span", { className: "szenen-bildwahl__titel", children: bild.title }),
17589
17820
  /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
17590
17821
  "button",
17591
17822
  {
@@ -17624,7 +17855,10 @@ var DeriveArea = ({ port, contextId }) => {
17624
17855
  );
17625
17856
  const gewaehlt = regeln.status === "ready" ? regeln.value.find((r) => r.versionId === policyVersionId) ?? null : null;
17626
17857
  async function starten(folgt) {
17627
- if (!gewaehlt) return;
17858
+ if (!gewaehlt) {
17859
+ setMeldung("Bitte zuerst eine Rekonstruktionsregel waehlen \u2014 ohne sie ist kein Lauf reproduzierbar (D-03).");
17860
+ return;
17861
+ }
17628
17862
  setLaeuft(true);
17629
17863
  setMeldung(null);
17630
17864
  setLauf(null);
@@ -18174,13 +18408,16 @@ var SzenenView = ({
18174
18408
  freezeDraftChain,
18175
18409
  freezeSceneWorkingState,
18176
18410
  getSzenenSchemas,
18411
+ gruppiereZuItems,
18177
18412
  hasPlacementCapability,
18178
18413
  isKnownElementType,
18179
18414
  isOk,
18180
18415
  isProjectionCurrent,
18181
18416
  isSzenenItemId,
18182
18417
  itemIdForRun,
18418
+ ladeVorschauen,
18183
18419
  linkFor,
18420
+ locatorTeile,
18184
18421
  logRevisionOf,
18185
18422
  mergeSzenenBatchOrigin,
18186
18423
  noticeFor,
package/dist/szenen.mjs CHANGED
@@ -102,13 +102,16 @@ import {
102
102
  freezeDraftChain,
103
103
  freezeSceneWorkingState,
104
104
  getSzenenSchemas,
105
+ gruppiereZuItems,
105
106
  hasPlacementCapability,
106
107
  isKnownElementType,
107
108
  isOk,
108
109
  isProjectionCurrent,
109
110
  isSzenenItemId,
110
111
  itemIdForRun,
112
+ ladeVorschauen,
111
113
  linkFor,
114
+ locatorTeile,
112
115
  logRevisionOf,
113
116
  mergeSzenenBatchOrigin,
114
117
  noticeFor,
@@ -145,7 +148,7 @@ import {
145
148
  validateElementVersion,
146
149
  validatePayload,
147
150
  withReference
148
- } from "./chunk-566GHYAT.mjs";
151
+ } from "./chunk-TW4IDHAN.mjs";
149
152
  import {
150
153
  ContractViolation,
151
154
  STYLE_PAYLOAD,
@@ -749,15 +752,26 @@ function createMemoryViewPort(options = {}) {
749
752
  }
750
753
  };
751
754
  const bilder = /* @__PURE__ */ new Map();
755
+ const werke = [];
752
756
  for (const bild of options.images ?? []) {
753
757
  bilder.set(bild.mediaRef, { bild, data: "AAAA" });
758
+ werke.push({ itemRef: bild.mediaRef, title: bild.title, images: [bild] });
754
759
  }
760
+ for (const werk of options.imageItems ?? []) {
761
+ for (const bild of werk.images) bilder.set(bild.mediaRef, { bild, data: "AAAA" });
762
+ werke.push(werk);
763
+ }
764
+ const vorschauLazy = options.imagePreviewLazy === true;
755
765
  const imageLibraryPort = {
756
- async listImages(query) {
757
- const alle = [...bilder.values()].map((e) => e.bild);
766
+ async listItems(query) {
758
767
  const gesucht = query?.search?.trim().toLowerCase();
759
- const gefiltert = gesucht ? alle.filter((b) => b.title.toLowerCase().includes(gesucht)) : alle;
760
- return ok(query?.limit ? gefiltert.slice(0, query.limit) : gefiltert);
768
+ const gefiltert = gesucht ? werke.filter((w) => w.title.toLowerCase().includes(gesucht)) : werke;
769
+ const sichtbar = query?.limit ? gefiltert.slice(0, query.limit) : gefiltert;
770
+ if (!vorschauLazy) return ok(sichtbar);
771
+ return ok(sichtbar.map((w) => ({
772
+ ...w,
773
+ images: w.images.map((b) => ({ ...b, thumbUrl: null }))
774
+ })));
761
775
  },
762
776
  async resolveImage(mediaRef) {
763
777
  const eintrag = bilder.get(mediaRef);
@@ -775,9 +789,19 @@ function createMemoryViewPort(options = {}) {
775
789
  thumbUrl: null
776
790
  };
777
791
  bilder.set(mediaRef, { bild, data: input.data });
792
+ werke.unshift({ itemRef: mediaRef, title: bild.title, images: [bild] });
778
793
  return done(bild);
779
794
  }
780
795
  };
796
+ if (vorschauLazy) {
797
+ imageLibraryPort.loadPreview = async (mediaRef) => {
798
+ const eintrag = bilder.get(mediaRef);
799
+ if (!eintrag) {
800
+ return { kind: "error", message: `Bild ${mediaRef} ist unbekannt.`, retryable: false };
801
+ }
802
+ return ok(`data:${eintrag.bild.mimeType};base64,${eintrag.data}`);
803
+ };
804
+ }
781
805
  const batches = [...options.batches ?? []];
782
806
  const imageGenerationEvents = createInMemoryEventChannel();
783
807
  const imageBatchHistoryPort = {
@@ -1684,13 +1708,16 @@ export {
1684
1708
  freezeDraftChain,
1685
1709
  freezeSceneWorkingState,
1686
1710
  getSzenenSchemas,
1711
+ gruppiereZuItems,
1687
1712
  hasPlacementCapability,
1688
1713
  isKnownElementType,
1689
1714
  isOk,
1690
1715
  isProjectionCurrent,
1691
1716
  isSzenenItemId,
1692
1717
  itemIdForRun,
1718
+ ladeVorschauen,
1693
1719
  linkFor,
1720
+ locatorTeile,
1694
1721
  logRevisionOf,
1695
1722
  mergeSzenenBatchOrigin,
1696
1723
  noticeFor,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rslsp1/fa-app-tools",
3
- "version": "4.0.0",
3
+ "version": "5.0.0",
4
4
  "description": "Shared tools and hooks for Fine Art flow apps",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",