@murumets-ee/media 0.72.0 → 0.73.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 (94) hide show
  1. package/dist/admin.d.mts.map +1 -1
  2. package/dist/admin.mjs +1 -1
  3. package/dist/backfill-B1StTnxg.mjs +2 -0
  4. package/dist/{backfill-D2Ubblh3.mjs.map → backfill-B1StTnxg.mjs.map} +1 -1
  5. package/dist/client.d.mts +16 -6
  6. package/dist/client.d.mts.map +1 -1
  7. package/dist/client.mjs +1 -1
  8. package/dist/client.mjs.map +1 -1
  9. package/dist/crop-editor.d.mts +6 -5
  10. package/dist/crop-editor.d.mts.map +1 -1
  11. package/dist/crop-editor.mjs +1 -1
  12. package/dist/crop-rect-B-AJ2pWm.mjs.map +1 -1
  13. package/dist/crop-rect-C-SbjoxK.d.mts.map +1 -1
  14. package/dist/{deps-CzHEhyJK.mjs → deps-DaGjHO16.mjs} +2 -2
  15. package/dist/{deps-CzHEhyJK.mjs.map → deps-DaGjHO16.mjs.map} +1 -1
  16. package/dist/en-Duj_Pp-O.mjs +2 -0
  17. package/dist/en-Duj_Pp-O.mjs.map +1 -0
  18. package/dist/et-BFdSZnb5.mjs +2 -0
  19. package/dist/et-BFdSZnb5.mjs.map +1 -0
  20. package/dist/generate-variants-Dpy4aT2o.mjs +2 -0
  21. package/dist/{generate-variants-zyPjaGcy.mjs.map → generate-variants-Dpy4aT2o.mjs.map} +1 -1
  22. package/dist/i18n.mjs +1 -1
  23. package/dist/image-styles-settings.d.mts +1 -1
  24. package/dist/image-styles-settings.d.mts.map +1 -1
  25. package/dist/image-styles-settings.mjs +1 -1
  26. package/dist/image-styles-settings.mjs.map +1 -1
  27. package/dist/image-styles.d.mts +1 -1
  28. package/dist/image-styles.mjs +1 -1
  29. package/dist/image-styles.mjs.map +1 -1
  30. package/dist/index.d.mts +38 -4
  31. package/dist/index.d.mts.map +1 -1
  32. package/dist/index.mjs +1 -1
  33. package/dist/index.mjs.map +1 -1
  34. package/dist/{media-config-DmcTxuDM.mjs → media-config-DkbnXIjC.mjs} +2 -2
  35. package/dist/{media-config-DmcTxuDM.mjs.map → media-config-DkbnXIjC.mjs.map} +1 -1
  36. package/dist/picker.d.mts +1 -1
  37. package/dist/plugin.d.mts +1 -1
  38. package/dist/plugin.mjs +1 -1
  39. package/dist/process-image-wQwiRhyk.mjs +2 -0
  40. package/dist/process-image-wQwiRhyk.mjs.map +1 -0
  41. package/dist/processing.d.mts +2 -2
  42. package/dist/processing.d.mts.map +1 -1
  43. package/dist/processing.mjs +1 -1
  44. package/dist/public-resolver.d.mts +80 -16
  45. package/dist/public-resolver.d.mts.map +1 -1
  46. package/dist/public-resolver.mjs +1 -1
  47. package/dist/public-resolver.mjs.map +1 -1
  48. package/dist/query-client.d.mts +1 -1
  49. package/dist/{regenerate-variants-Cp3sNHkT.mjs → regenerate-variants-RwV_AQSo.mjs} +2 -2
  50. package/dist/{regenerate-variants-Cp3sNHkT.mjs.map → regenerate-variants-RwV_AQSo.mjs.map} +1 -1
  51. package/dist/{register-BSGkELj0.mjs → register-CJKjNl2w.mjs} +2 -2
  52. package/dist/{register-BSGkELj0.mjs.map → register-CJKjNl2w.mjs.map} +1 -1
  53. package/dist/{resolve-image-styles-pf7Nd3Zf.mjs → resolve-image-styles-YExI329D.mjs} +2 -2
  54. package/dist/{resolve-image-styles-pf7Nd3Zf.mjs.map → resolve-image-styles-YExI329D.mjs.map} +1 -1
  55. package/dist/routes-DaE0M8h8.mjs +2 -0
  56. package/dist/routes-DaE0M8h8.mjs.map +1 -0
  57. package/dist/ru-ojvshRJy.mjs +2 -0
  58. package/dist/ru-ojvshRJy.mjs.map +1 -0
  59. package/dist/{schedule-DREjg2ji.mjs → schedule-JlvfUEVA.mjs} +2 -2
  60. package/dist/{schedule-DREjg2ji.mjs.map → schedule-JlvfUEVA.mjs.map} +1 -1
  61. package/dist/shapes-BpOEJeLx.mjs +2 -0
  62. package/dist/shapes-BpOEJeLx.mjs.map +1 -0
  63. package/dist/{shapes-40dHtTb-.d.mts → shapes-C51rXSDJ.d.mts} +2 -2
  64. package/dist/{shapes-40dHtTb-.d.mts.map → shapes-C51rXSDJ.d.mts.map} +1 -1
  65. package/dist/shapes-CqppzFzb.mjs +2 -0
  66. package/dist/shapes-CqppzFzb.mjs.map +1 -0
  67. package/dist/{types-CgkJF5dc.d.mts → types-6PZQ4Zq6.d.mts} +8 -11
  68. package/dist/{types-CgkJF5dc.d.mts.map → types-6PZQ4Zq6.d.mts.map} +1 -1
  69. package/dist/{types-DwzblZfW.d.mts → types-Lj3SkZQ_.d.mts} +11 -1
  70. package/dist/types-Lj3SkZQ_.d.mts.map +1 -0
  71. package/dist/variant-plan-86tJKtVi.mjs +2 -0
  72. package/dist/variant-plan-86tJKtVi.mjs.map +1 -0
  73. package/dist/variant-state-CBtqb9Cb.mjs +2 -0
  74. package/dist/variant-state-CBtqb9Cb.mjs.map +1 -0
  75. package/package.json +9 -9
  76. package/dist/backfill-D2Ubblh3.mjs +0 -2
  77. package/dist/en-A8YzaZ51.mjs +0 -2
  78. package/dist/en-A8YzaZ51.mjs.map +0 -1
  79. package/dist/et-CXvu8U31.mjs +0 -2
  80. package/dist/et-CXvu8U31.mjs.map +0 -1
  81. package/dist/generate-variants-zyPjaGcy.mjs +0 -2
  82. package/dist/process-image-Deedtkh4.mjs +0 -2
  83. package/dist/process-image-Deedtkh4.mjs.map +0 -1
  84. package/dist/routes-BJ23Cp0g.mjs +0 -2
  85. package/dist/routes-BJ23Cp0g.mjs.map +0 -1
  86. package/dist/ru-Dj-ax8s_.mjs +0 -2
  87. package/dist/ru-Dj-ax8s_.mjs.map +0 -1
  88. package/dist/shapes-BlKW-0C1.mjs +0 -2
  89. package/dist/shapes-BlKW-0C1.mjs.map +0 -1
  90. package/dist/types-DwzblZfW.d.mts.map +0 -1
  91. package/dist/variant-key-Cki4n-42.mjs +0 -2
  92. package/dist/variant-key-Cki4n-42.mjs.map +0 -1
  93. package/dist/variant-plan-DV8hsMdz.mjs +0 -2
  94. package/dist/variant-plan-DV8hsMdz.mjs.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"generate-variants-zyPjaGcy.mjs","names":[],"sources":["../src/jobs/generate-variants.ts"],"sourcesContent":["/**\n * One generation pass for one photograph — the body of the\n * `media:generate-variants` job (images D004), and of the inline fallback an\n * app without the queue plugin runs at upload.\n *\n * ## The order, which is the point (F002)\n *\n * 1. Read the row and the ORIGINAL; correct the row's drawn size and seed a\n * missing focal point (F014, below).\n * 2. Plan the shaped set from the FILE's drawn size (F016) and render only the\n * variants whose key does not exist yet. A shaped key is recipe-addressed\n * (D005), so an existing key already holds the right bytes.\n * 3. Upload the new files.\n * 4. Record the new set on the original's metadata — the pointer readers use.\n * 5. Only then delete what the new set superseded.\n *\n * Today's `regenerateAllVariants` deleted first, so removing a style destroyed\n * its files before anything replaced them and every variant URL 404'd for the\n * length of the run. Here a reader holding a stale URL keeps getting the old\n * picture until the pointer has moved.\n *\n * Legacy `{style}_{base}` keys cannot be additive — the key has no recipe in it\n * and `StorageClient.upload` refuses to overwrite a key (R014). So a legacy file\n * is re-rendered only when its style's recipe CHANGED (`legacyRecipes`), and\n * then replaced one key at a time with the new bytes already in memory: a\n * window of one upload, not of the whole library.\n *\n * ## The row, while the original is open anyway (F014)\n *\n * Rows uploaded before images PR02 hold sensor-oriented `width`/`height` — an\n * EXIF-rotated phone portrait stored 4000×3000 is drawn 3000×4000, and its focal\n * crop is positioned on the wrong axis. Rows uploaded before PR02 also have no\n * focal point. The pass already decodes the original, so it writes the drawn\n * size back and seeds a `null` focal point with `detectFocalPoint` — never\n * overwriting a point someone set (the picker's Reset writes the centre, not\n * `null`, so `null` means \"never set\"). Both go through `updateInternal`, which\n * the regenerate-on-edit behavior ignores, so this write does not enqueue\n * another pass of itself.\n *\n * ## Failure\n *\n * A variant that fails to upload is logged and the pass THROWS after recording\n * and deleting nothing a reader resolves — so the queue retries it, and the\n * retry only renders what is still missing. The shaped set readers see is left\n * exactly as it was; what did land is recorded as `unconfirmed` (adopted by the\n * retry, and deleted with the photograph). The one exception is a legacy file\n * replaced in place whose new upload failed after the old was deleted: its\n * mapping is dropped, so readers fall back to the original rather than a 404.\n *\n * ## Concurrency, and what is accepted\n *\n * Two passes for one photograph converge: a key already written by the other\n * fails the unique-key upload and is adopted on retry. A pass that sees the\n * row's point or crop change between planning and recording returns `stale`\n * and records nothing shaped. What is NOT closed: a newer pass recording\n * between this pass's freshness check and its own record — a window of two\n * round trips. The loser's set is then recorded for the winner's point until\n * the next backfill finds the mismatch (`isUpToDate`) and queues a repair.\n */\n\nimport type { Logger } from '@murumets-ee/core'\nimport type { FileRecord, StorageClient } from '@murumets-ee/storage'\nimport {\n detectFocalPoint,\n getImageDimensions,\n isProcessableImage,\n processImage,\n renderPlannedVariants,\n} from '../process-image.js'\nimport type { VariantConfig } from '../shapes.js'\nimport type { ImageStyle } from '../types.js'\nimport { deriveShapedVariantKey, deriveVariantKey } from '../variant-key.js'\nimport { type PlannedVariant, planVariants } from '../variant-plan.js'\nimport { deleteOwnedVariants } from './owned-delete.js'\nimport {\n FOCAL_SEED_TRIED,\n legacyRecipe,\n readVariantState,\n type StoredShapedVariant,\n type VariantState,\n writeVariantState,\n} from './variant-state.js'\n\n/** The slice of a media row a pass reads. `crops` arrives with images PR03; read leniently. */\nexport interface VariantSourceRow {\n readonly id: string\n readonly fileKey: string\n readonly filename: string\n readonly mimeType: string\n readonly mediaType: string\n readonly width?: number | null\n readonly height?: number | null\n readonly focalPoint?: unknown\n readonly crops?: unknown\n}\n\nexport interface VariantGenerationDeps {\n /** Reads the media row, and writes the corrected size / seeded focal point. */\n readonly media: {\n findById(id: string): Promise<VariantSourceRow | null>\n updateInternal(id: string, data: Record<string, unknown>): Promise<unknown>\n }\n readonly storage: Pick<\n StorageClient,\n 'download' | 'getMetadata' | 'upload' | 'updateMetadata' | 'delete'\n >\n /** The shape model — `resolveVariantConfig`. */\n readonly config: VariantConfig\n /** The legacy fixed styles — `resolveImageStyles`. */\n readonly styles: Readonly<Record<string, ImageStyle>>\n readonly logger?: Pick<Logger, 'info' | 'warn' | 'debug'> | undefined\n}\n\nexport type VariantGenerationStatus =\n /** The pass ran — possibly finding nothing to do. */\n | 'generated'\n /** Not a raster image this pipeline processes (SVG, GIF, a PDF), or its original is gone. */\n | 'skipped'\n /** The row no longer exists. */\n | 'missing'\n /**\n * The row's focal point or crops changed while the pass ran, so what it\n * rendered is already out of date. Nothing shaped was recorded; the edit\n * enqueued its own pass, which will.\n */\n | 'stale'\n\nexport interface VariantGenerationResult {\n readonly mediaId: string\n readonly status: VariantGenerationStatus\n /** Files written this pass (shaped + legacy). */\n readonly uploaded: number\n /** Planned shaped variants that already existed and were kept as they were. */\n readonly reused: number\n /** Superseded files removed after the new set was recorded. */\n readonly deleted: number\n /** Variants the per-image cap cut off (`MAX_VARIANTS_PER_IMAGE`). */\n readonly dropped: number\n readonly dimensionsCorrected: boolean\n readonly focalPointSeeded: boolean\n}\n\n/** Uploads run a few at a time — each is a DB insert plus an object write. */\nconst UPLOAD_CONCURRENCY = 4\n\n/** Thrown when some variants failed to upload — the queue retries the pass. */\nexport class VariantUploadError extends Error {\n readonly failed: number\n constructor(mediaId: string, failed: number) {\n super(`media ${mediaId}: ${failed} variant(s) failed to upload; the pass will be retried`)\n this.name = 'VariantUploadError'\n this.failed = failed\n }\n}\n\nfunction result(\n mediaId: string,\n status: VariantGenerationStatus,\n partial: Partial<Omit<VariantGenerationResult, 'mediaId' | 'status'>> = {},\n): VariantGenerationResult {\n return {\n mediaId,\n status,\n uploaded: 0,\n reused: 0,\n deleted: 0,\n dropped: 0,\n dimensionsCorrected: false,\n focalPointSeeded: false,\n ...partial,\n }\n}\n\n/** A downloaded body as one Buffer. */\nasync function toBuffer(body: Buffer | ReadableStream<Uint8Array>): Promise<Buffer> {\n if (Buffer.isBuffer(body)) return body\n const chunks: Uint8Array[] = []\n const reader = body.getReader()\n for (;;) {\n const { done, value } = await reader.read()\n if (done) break\n if (value) chunks.push(value)\n }\n return Buffer.concat(chunks)\n}\n\n/** `Promise.all(items.map(fn))` with at most `limit` in flight. */\nasync function eachBounded<T>(\n items: readonly T[],\n limit: number,\n fn: (item: T) => Promise<void>,\n): Promise<void> {\n const queue = [...items]\n const worker = async (): Promise<void> => {\n for (let item = queue.shift(); item !== undefined; item = queue.shift()) await fn(item)\n }\n await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker))\n}\n\nfunction sameJson(a: unknown, b: unknown): boolean {\n return JSON.stringify(a ?? null) === JSON.stringify(b ?? null)\n}\n\nfunction shapedKey(fileKey: string, planned: PlannedVariant): string {\n return deriveShapedVariantKey(fileKey, {\n shape: planned.shape,\n width: planned.width,\n fingerprint: planned.fingerprint,\n format: planned.format,\n })\n}\n\n/**\n * A shaped file that exists but was not recorded — a previous pass uploaded it\n * and then failed before recording, or a concurrent pass did. The job writes the\n * real size onto every file it uploads, so it can be recorded without decoding.\n */\nfunction recordedFromFile(\n file: FileRecord,\n planned: PlannedVariant,\n key: string,\n): StoredShapedVariant | null {\n const meta = file.metadata ?? {}\n const { width, height } = meta\n if (typeof width !== 'number' || typeof height !== 'number' || width <= 0 || height <= 0) {\n return null\n }\n return { shape: planned.shape, rung: planned.width, width, height, format: planned.format, key }\n}\n\n/** Generate — and record, and clean up after — every variant one photograph should have. */\nexport async function generateMediaVariants(\n mediaId: string,\n deps: VariantGenerationDeps,\n): Promise<VariantGenerationResult> {\n const { media, storage, config, styles, logger } = deps\n\n const found = await media.findById(mediaId)\n if (!found) return result(mediaId, 'missing')\n let row = found\n if (row.mediaType !== 'image' || !isProcessableImage(row.mimeType)) {\n return result(mediaId, 'skipped')\n }\n const original = await storage.getMetadata(row.fileKey)\n if (!original) {\n logger?.warn({ mediaId }, 'media variants: original file record is missing; nothing to do')\n return result(mediaId, 'skipped')\n }\n const buffer = await toBuffer((await storage.download(row.fileKey)).body)\n\n // ── 1. The row: drawn size, and a seed for a point nobody has set (F014) ──\n let seedTried = false\n const drawn = await getImageDimensions(buffer)\n const patch: Record<string, unknown> = {}\n if (drawn.width > 0 && (row.width !== drawn.width || row.height !== drawn.height)) {\n patch.width = drawn.width\n patch.height = drawn.height\n }\n if (row.focalPoint === null || row.focalPoint === undefined) {\n const seeded = await detectFocalPoint(buffer).catch((err: unknown) => {\n logger?.warn({ mediaId, err }, 'media variants: focal-point detection failed; left unset')\n return null\n })\n if (seeded) patch.focalPoint = seeded\n // Nothing to guess about (a strip, a flat image): say so on the original,\n // or every walk would queue this photograph again to guess once more.\n else seedTried = true\n }\n if ('focalPoint' in patch) {\n // The seed took a download and a saliency pass; an editor may have set a\n // point meanwhile. Re-read right before writing, and never overwrite one\n // that is now set (a window of one round trip remains — accepted).\n const current = await media.findById(mediaId)\n if (!current) return result(mediaId, 'missing')\n if (current.focalPoint !== null && current.focalPoint !== undefined) {\n delete patch.focalPoint\n row = { ...row, focalPoint: current.focalPoint, crops: current.crops }\n }\n }\n if (Object.keys(patch).length > 0) {\n await media.updateInternal(mediaId, patch)\n row = { ...row, ...patch }\n }\n\n // ── 2. The shaped plan, from the FILE's drawn size ──\n const plan = planVariants(drawn, config, { focalPoint: row.focalPoint, crops: row.crops })\n if (plan.dropped > 0) {\n logger?.warn(\n { mediaId, dropped: plan.dropped },\n 'media variants: the per-image cap cut the largest variants off this photograph',\n )\n }\n\n const before = readVariantState(original.metadata)\n const recorded = new Map(before.shaped.map((v) => [v.key, v]))\n const present: StoredShapedVariant[] = []\n const toRender: Array<{ planned: PlannedVariant; key: string; replace: boolean }> = []\n for (const planned of plan.variants) {\n const key = shapedKey(row.fileKey, planned)\n const known = recorded.get(key)\n if (known) {\n present.push({ ...known, shape: planned.shape, rung: planned.width })\n continue\n }\n const existing = await storage.getMetadata(key)\n const fromFile = existing ? recordedFromFile(existing, planned, key) : null\n if (fromFile) present.push(fromFile)\n else toRender.push({ planned, key, replace: existing !== null })\n }\n const reused = present.length\n\n // ── 3. Render what is missing, and upload it ──\n let uploaded = 0\n let failed = 0\n const uploadedKeys: string[] = []\n /** Legacy files written this pass — only for the photograph-deleted-mid-pass cleanup. */\n const uploadedLegacyKeys: string[] = []\n const rendered = await renderPlannedVariants(\n buffer,\n toRender.map((t) => t.planned),\n )\n const work = toRender.map((t, i) => ({ ...t, variant: rendered[i] }))\n await eachBounded(work, UPLOAD_CONCURRENCY, async ({ planned, key, replace, variant }) => {\n if (!variant) return\n try {\n // An unrecorded file with no size on it predates this module's metadata\n // and cannot be trusted as the recipe's bytes — replaced, not kept.\n if (replace) await storage.delete(key)\n await storage.upload(variant.buffer, {\n key,\n filename: `${planned.shape}_${planned.width}w_${row.filename}`,\n mimeType: variant.mimeType,\n size: variant.buffer.byteLength,\n visibility: original.visibility,\n metadata: {\n variantOf: row.fileKey,\n shape: planned.shape,\n rung: planned.width,\n width: variant.width,\n height: variant.height,\n },\n })\n present.push({\n shape: planned.shape,\n rung: planned.width,\n width: variant.width,\n height: variant.height,\n format: planned.format,\n key,\n })\n uploadedKeys.push(key)\n uploaded++\n } catch (err) {\n failed++\n logger?.warn({ mediaId, key, err }, 'media variants: shaped variant upload failed')\n }\n })\n\n // ── Legacy fixed styles: re-rendered only when the recipe changed ──\n const legacy = new Map<string, string>()\n const legacyRecipes = new Map<string, string>()\n const legacyDue: Record<string, ImageStyle> = {}\n /** Styles whose old file was deleted for an in-place replacement whose upload then failed. */\n const legacyGone = new Set<string>()\n for (const [name, style] of Object.entries(styles)) {\n const key = deriveVariantKey(row.fileKey, name, style.format ?? 'webp')\n const recipe = legacyRecipe(style)\n if (before.legacy.get(name) === key && before.legacyRecipes.get(name) === recipe) {\n legacy.set(name, key)\n legacyRecipes.set(name, recipe)\n } else {\n legacyDue[name] = style\n }\n }\n if (Object.keys(legacyDue).length > 0) {\n const processed = await processImage(buffer, legacyDue)\n await eachBounded([...processed.variants], UPLOAD_CONCURRENCY, async ([name, variant]) => {\n const style = legacyDue[name]\n if (!style) return\n const key = deriveVariantKey(row.fileKey, name, variant.format)\n let oldDeleted = false\n try {\n // In place: a legacy key names no recipe, so new bytes need the old row\n // gone first. The bytes are already rendered — the gap is one upload.\n if (await storage.getMetadata(key)) {\n await storage.delete(key)\n oldDeleted = true\n }\n await storage.upload(variant.buffer, {\n key,\n filename: `${name}_${row.filename}`,\n mimeType: variant.mimeType,\n size: variant.buffer.byteLength,\n visibility: original.visibility,\n metadata: { variantOf: row.fileKey, style: name },\n })\n legacy.set(name, key)\n legacyRecipes.set(name, legacyRecipe(style))\n uploadedLegacyKeys.push(key)\n uploaded++\n } catch (err) {\n failed++\n // The old file is gone and its replacement is not there: stop pointing\n // at it, so readers fall back to the original until the retry lands.\n if (oldDeleted) legacyGone.add(name)\n logger?.warn({ mediaId, key, err }, 'media variants: legacy style upload failed')\n }\n })\n }\n\n // ── Was the plan still the right one? ──\n const fresh = await media.findById(mediaId)\n if (!fresh) {\n // Deleted while the pass ran. `MediaClient.delete` removed what was\n // recorded; what this pass wrote was never recorded, so it goes here.\n await deleteOwnedVariants(\n storage,\n row.fileKey,\n [...uploadedKeys, ...uploadedLegacyKeys],\n logger,\n )\n return result(mediaId, 'missing', { uploaded })\n }\n const stale = !sameJson(fresh.focalPoint, row.focalPoint) || !sameJson(fresh.crops, row.crops)\n\n // ── 4. Record — merged over a fresh read, so a concurrent writer's other keys survive ──\n const latest = (await storage.getMetadata(row.fileKey)) ?? original\n const latestState = readVariantState(latest.metadata)\n const mergedLegacy = without(merged(latestState.legacy, legacy), legacyGone)\n const mergedRecipes = without(merged(latestState.legacyRecipes, legacyRecipes), legacyGone)\n // A legacy style whose key CHANGED (its format did) now points at the new\n // file; the old one is recorded nowhere else. Outside a successful pass it\n // must stay findable, so it goes to `unconfirmed` (deleted by the next\n // successful pass, and with the photograph).\n const replacedLegacy = [...latestState.legacy]\n .filter(([name, key]) => mergedLegacy.has(name) && mergedLegacy.get(name) !== key)\n .map(([, key]) => key)\n const freshKeys = stale\n ? new Set(\n planVariants(drawn, config, {\n focalPoint: fresh.focalPoint,\n crops: fresh.crops,\n }).variants.map((p) => shapedKey(row.fileKey, p)),\n )\n : null\n /** Every file this pass holds that the recorded `shaped` set will not name. */\n const leftovers = (shaped: readonly StoredShapedVariant[], extra: readonly string[]) => {\n const named = new Set(shaped.map((v) => v.key))\n return [...new Set([...latestState.unconfirmed, ...extra, ...replacedLegacy])].filter(\n (k) => !named.has(k),\n )\n }\n let next: VariantState\n if (stale) {\n // The shaped set belongs to the edit's own pass. Record the legacy files\n // (independent of focal point and crops). What this pass rendered that the\n // NEW point also plans is kept as unconfirmed for the edit's pass to adopt;\n // the rest is deleted below.\n const keepForEdit = uploadedKeys.filter((k) => freshKeys?.has(k))\n next = {\n legacy: mergedLegacy,\n legacyRecipes: mergedRecipes,\n shaped: latestState.shaped,\n unconfirmed: leftovers(latestState.shaped, keepForEdit),\n }\n } else if (failed > 0) {\n // Never two crops for one (shape, rung): a variant that landed is promoted\n // only where readers have nothing for its (shape, rung) yet — so a photo\n // whose one format always fails still gets every other size. The rest is\n // UNCONFIRMED: the retry adopts it, and deleting the photograph finds it.\n const taken = new Set(latestState.shaped.map((v) => `${v.shape}@${v.rung}`))\n const promoted = present.filter((v) => !taken.has(`${v.shape}@${v.rung}`))\n const shaped = sortShaped([...latestState.shaped, ...promoted])\n next = {\n legacy: mergedLegacy,\n legacyRecipes: mergedRecipes,\n shaped,\n unconfirmed: leftovers(\n shaped,\n present.map((v) => v.key),\n ),\n }\n } else {\n next = { legacy, legacyRecipes, shaped: sortShaped(present), unconfirmed: [] }\n }\n const written = writeVariantState(latest.metadata, next)\n await storage.updateMetadata(row.fileKey, {\n metadata: seedTried ? { ...written, [FOCAL_SEED_TRIED]: true } : written,\n })\n\n // ── 5. Only now delete what the new set superseded — and only files that are\n // variants OF this original (`deleteOwnedVariants`): the lists come from\n // caller-writable metadata. ──\n const toDelete = new Set<string>()\n if (stale) {\n const stillRecorded = new Set(latestState.shaped.map((v) => v.key))\n for (const key of uploadedKeys) {\n if (!freshKeys?.has(key) && !stillRecorded.has(key)) toDelete.add(key)\n }\n } else if (failed === 0) {\n const keep = new Set([...present.map((v) => v.key), ...legacy.values()])\n for (const v of latestState.shaped) if (!keep.has(v.key)) toDelete.add(v.key)\n for (const key of latestState.legacy.values()) if (!keep.has(key)) toDelete.add(key)\n for (const key of latestState.unconfirmed) if (!keep.has(key)) toDelete.add(key)\n }\n const deleted = await deleteOwnedVariants(storage, row.fileKey, toDelete, logger)\n\n const outcome = result(mediaId, stale ? 'stale' : 'generated', {\n uploaded,\n reused,\n deleted,\n dropped: plan.dropped,\n dimensionsCorrected: 'width' in patch,\n focalPointSeeded: 'focalPoint' in patch,\n })\n if (failed > 0 && !stale) throw new VariantUploadError(mediaId, failed)\n logger?.debug(outcome, 'media variants: pass complete')\n return outcome\n}\n\nfunction merged<V>(base: ReadonlyMap<string, V>, over: ReadonlyMap<string, V>): Map<string, V> {\n return new Map([...base, ...over])\n}\n\nfunction without<V>(map: Map<string, V>, names: ReadonlySet<string>): Map<string, V> {\n for (const name of names) map.delete(name)\n return map\n}\n\n/** Shape, then width — a stable order, so an unchanged set writes unchanged JSON. */\nfunction sortShaped(list: readonly StoredShapedVariant[]): StoredShapedVariant[] {\n return [...list].sort((x, y) =>\n x.shape === y.shape ? x.rung - y.rung || x.width - y.width : x.shape < y.shape ? -1 : 1,\n )\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"generate-variants-Dpy4aT2o.mjs","names":[],"sources":["../src/jobs/generate-variants.ts"],"sourcesContent":["/**\n * One generation pass for one photograph — the body of the\n * `media:generate-variants` job (images D004), and of the inline fallback an\n * app without the queue plugin runs at upload.\n *\n * ## The order, which is the point (F002)\n *\n * 1. Read the row and the ORIGINAL; correct the row's drawn size and seed a\n * missing focal point (F014, below).\n * 2. Plan the shaped set from the FILE's drawn size (F016) and render only the\n * variants whose key does not exist yet. A shaped key is recipe-addressed\n * (D005), so an existing key already holds the right bytes.\n * 3. Upload the new files.\n * 4. Record the new set on the original's metadata — the pointer readers use.\n * 5. Only then delete what the new set superseded.\n *\n * Today's `regenerateAllVariants` deleted first, so removing a style destroyed\n * its files before anything replaced them and every variant URL 404'd for the\n * length of the run. Here a reader holding a stale URL keeps getting the old\n * picture until the pointer has moved.\n *\n * Legacy `{style}_{base}` keys cannot be additive — the key has no recipe in it\n * and `StorageClient.upload` refuses to overwrite a key (R014). So a legacy file\n * is re-rendered only when its style's recipe CHANGED (`legacyRecipes`), and\n * then replaced one key at a time with the new bytes already in memory: a\n * window of one upload, not of the whole library.\n *\n * ## The row, while the original is open anyway (F014)\n *\n * Rows uploaded before images PR02 hold sensor-oriented `width`/`height` — an\n * EXIF-rotated phone portrait stored 4000×3000 is drawn 3000×4000, and its focal\n * crop is positioned on the wrong axis. Rows uploaded before PR02 also have no\n * focal point. The pass already decodes the original, so it writes the drawn\n * size back and seeds a `null` focal point with `detectFocalPoint` — never\n * overwriting a point someone set (the picker's Reset writes the centre, not\n * `null`, so `null` means \"never set\"). Both go through `updateInternal`, which\n * the regenerate-on-edit behavior ignores, so this write does not enqueue\n * another pass of itself.\n *\n * ## Failure\n *\n * A variant that fails to upload is logged and the pass THROWS after recording\n * and deleting nothing a reader resolves — so the queue retries it, and the\n * retry only renders what is still missing. The shaped set readers see is left\n * exactly as it was; what did land is recorded as `unconfirmed` (adopted by the\n * retry, and deleted with the photograph). The one exception is a legacy file\n * replaced in place whose new upload failed after the old was deleted: its\n * mapping is dropped, so readers fall back to the original rather than a 404.\n *\n * ## Concurrency, and what is accepted\n *\n * Two passes for one photograph converge: a key already written by the other\n * fails the unique-key upload and is adopted on retry. A pass that sees the\n * row's point or crop change between planning and recording returns `stale`\n * and records nothing shaped. What is NOT closed: a newer pass recording\n * between this pass's freshness check and its own record — a window of two\n * round trips. The loser's set is then recorded for the winner's point until\n * the next backfill finds the mismatch (`isUpToDate`) and queues a repair.\n */\n\nimport type { Logger } from '@murumets-ee/core'\nimport type { FileRecord, StorageClient } from '@murumets-ee/storage'\nimport {\n detectFocalPoint,\n getImageDimensions,\n isProcessableImage,\n processImage,\n renderPlannedVariants,\n} from '../process-image.js'\nimport type { VariantConfig } from '../shapes.js'\nimport type { ImageStyle } from '../types.js'\nimport { deriveShapedVariantKey, deriveVariantKey } from '../variant-key.js'\nimport { type PlannedVariant, planVariants } from '../variant-plan.js'\nimport { deleteOwnedVariants } from './owned-delete.js'\nimport {\n FOCAL_SEED_TRIED,\n legacyRecipe,\n readVariantState,\n type StoredShapedVariant,\n type VariantState,\n writeVariantState,\n} from './variant-state.js'\n\n/** The slice of a media row a pass reads. `crops` arrives with images PR03; read leniently. */\nexport interface VariantSourceRow {\n readonly id: string\n readonly fileKey: string\n readonly filename: string\n readonly mimeType: string\n readonly mediaType: string\n readonly width?: number | null\n readonly height?: number | null\n readonly focalPoint?: unknown\n readonly crops?: unknown\n}\n\nexport interface VariantGenerationDeps {\n /** Reads the media row, and writes the corrected size / seeded focal point. */\n readonly media: {\n findById(id: string): Promise<VariantSourceRow | null>\n updateInternal(id: string, data: Record<string, unknown>): Promise<unknown>\n }\n readonly storage: Pick<\n StorageClient,\n 'download' | 'getMetadata' | 'upload' | 'updateMetadata' | 'delete'\n >\n /** The shape model — `resolveVariantConfig`. */\n readonly config: VariantConfig\n /** The legacy fixed styles — `resolveImageStyles`. */\n readonly styles: Readonly<Record<string, ImageStyle>>\n readonly logger?: Pick<Logger, 'info' | 'warn' | 'debug'> | undefined\n}\n\nexport type VariantGenerationStatus =\n /** The pass ran — possibly finding nothing to do. */\n | 'generated'\n /** Not a raster image this pipeline processes (SVG, GIF, a PDF), or its original is gone. */\n | 'skipped'\n /** The row no longer exists. */\n | 'missing'\n /**\n * The row's focal point or crops changed while the pass ran, so what it\n * rendered is already out of date. Nothing shaped was recorded; the edit\n * enqueued its own pass, which will.\n */\n | 'stale'\n\nexport interface VariantGenerationResult {\n readonly mediaId: string\n readonly status: VariantGenerationStatus\n /** Files written this pass (shaped + legacy). */\n readonly uploaded: number\n /** Planned shaped variants that already existed and were kept as they were. */\n readonly reused: number\n /** Superseded files removed after the new set was recorded. */\n readonly deleted: number\n /** Variants the per-image cap cut off (`MAX_VARIANTS_PER_IMAGE`). */\n readonly dropped: number\n readonly dimensionsCorrected: boolean\n readonly focalPointSeeded: boolean\n}\n\n/** Uploads run a few at a time — each is a DB insert plus an object write. */\nconst UPLOAD_CONCURRENCY = 4\n\n/** Thrown when some variants failed to upload — the queue retries the pass. */\nexport class VariantUploadError extends Error {\n readonly failed: number\n constructor(mediaId: string, failed: number) {\n super(`media ${mediaId}: ${failed} variant(s) failed to upload; the pass will be retried`)\n this.name = 'VariantUploadError'\n this.failed = failed\n }\n}\n\nfunction result(\n mediaId: string,\n status: VariantGenerationStatus,\n partial: Partial<Omit<VariantGenerationResult, 'mediaId' | 'status'>> = {},\n): VariantGenerationResult {\n return {\n mediaId,\n status,\n uploaded: 0,\n reused: 0,\n deleted: 0,\n dropped: 0,\n dimensionsCorrected: false,\n focalPointSeeded: false,\n ...partial,\n }\n}\n\n/** A downloaded body as one Buffer. */\nasync function toBuffer(body: Buffer | ReadableStream<Uint8Array>): Promise<Buffer> {\n if (Buffer.isBuffer(body)) return body\n const chunks: Uint8Array[] = []\n const reader = body.getReader()\n for (;;) {\n const { done, value } = await reader.read()\n if (done) break\n if (value) chunks.push(value)\n }\n return Buffer.concat(chunks)\n}\n\n/** `Promise.all(items.map(fn))` with at most `limit` in flight. */\nasync function eachBounded<T>(\n items: readonly T[],\n limit: number,\n fn: (item: T) => Promise<void>,\n): Promise<void> {\n const queue = [...items]\n const worker = async (): Promise<void> => {\n for (let item = queue.shift(); item !== undefined; item = queue.shift()) await fn(item)\n }\n await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker))\n}\n\nfunction sameJson(a: unknown, b: unknown): boolean {\n return JSON.stringify(a ?? null) === JSON.stringify(b ?? null)\n}\n\nfunction shapedKey(fileKey: string, planned: PlannedVariant): string {\n return deriveShapedVariantKey(fileKey, {\n shape: planned.shape,\n width: planned.width,\n fingerprint: planned.fingerprint,\n format: planned.format,\n })\n}\n\n/**\n * A shaped file that exists but was not recorded — a previous pass uploaded it\n * and then failed before recording, or a concurrent pass did. The job writes the\n * real size onto every file it uploads, so it can be recorded without decoding.\n */\nfunction recordedFromFile(\n file: FileRecord,\n planned: PlannedVariant,\n key: string,\n): StoredShapedVariant | null {\n const meta = file.metadata ?? {}\n const { width, height } = meta\n if (typeof width !== 'number' || typeof height !== 'number' || width <= 0 || height <= 0) {\n return null\n }\n return { shape: planned.shape, rung: planned.width, width, height, format: planned.format, key }\n}\n\n/** Generate — and record, and clean up after — every variant one photograph should have. */\nexport async function generateMediaVariants(\n mediaId: string,\n deps: VariantGenerationDeps,\n): Promise<VariantGenerationResult> {\n const { media, storage, config, styles, logger } = deps\n\n const found = await media.findById(mediaId)\n if (!found) return result(mediaId, 'missing')\n let row = found\n if (row.mediaType !== 'image' || !isProcessableImage(row.mimeType)) {\n return result(mediaId, 'skipped')\n }\n const original = await storage.getMetadata(row.fileKey)\n if (!original) {\n logger?.warn({ mediaId }, 'media variants: original file record is missing; nothing to do')\n return result(mediaId, 'skipped')\n }\n const buffer = await toBuffer((await storage.download(row.fileKey)).body)\n\n // ── 1. The row: drawn size, and a seed for a point nobody has set (F014) ──\n let seedTried = false\n const drawn = await getImageDimensions(buffer)\n const patch: Record<string, unknown> = {}\n if (drawn.width > 0 && (row.width !== drawn.width || row.height !== drawn.height)) {\n patch.width = drawn.width\n patch.height = drawn.height\n }\n if (row.focalPoint === null || row.focalPoint === undefined) {\n const seeded = await detectFocalPoint(buffer).catch((err: unknown) => {\n logger?.warn({ mediaId, err }, 'media variants: focal-point detection failed; left unset')\n return null\n })\n if (seeded) patch.focalPoint = seeded\n // Nothing to guess about (a strip, a flat image): say so on the original,\n // or every walk would queue this photograph again to guess once more.\n else seedTried = true\n }\n if ('focalPoint' in patch) {\n // The seed took a download and a saliency pass; an editor may have set a\n // point meanwhile. Re-read right before writing, and never overwrite one\n // that is now set (a window of one round trip remains — accepted).\n const current = await media.findById(mediaId)\n if (!current) return result(mediaId, 'missing')\n if (current.focalPoint !== null && current.focalPoint !== undefined) {\n delete patch.focalPoint\n row = { ...row, focalPoint: current.focalPoint, crops: current.crops }\n }\n }\n if (Object.keys(patch).length > 0) {\n await media.updateInternal(mediaId, patch)\n row = { ...row, ...patch }\n }\n\n // ── 2. The shaped plan, from the FILE's drawn size ──\n const plan = planVariants(drawn, config, { focalPoint: row.focalPoint, crops: row.crops })\n if (plan.dropped > 0) {\n logger?.warn(\n { mediaId, dropped: plan.dropped },\n 'media variants: the per-image cap cut the largest variants off this photograph',\n )\n }\n\n const before = readVariantState(original.metadata)\n const recorded = new Map(before.shaped.map((v) => [v.key, v]))\n const present: StoredShapedVariant[] = []\n const toRender: Array<{ planned: PlannedVariant; key: string; replace: boolean }> = []\n for (const planned of plan.variants) {\n const key = shapedKey(row.fileKey, planned)\n const known = recorded.get(key)\n if (known) {\n present.push({ ...known, shape: planned.shape, rung: planned.width })\n continue\n }\n const existing = await storage.getMetadata(key)\n const fromFile = existing ? recordedFromFile(existing, planned, key) : null\n if (fromFile) present.push(fromFile)\n else toRender.push({ planned, key, replace: existing !== null })\n }\n const reused = present.length\n\n // ── 3. Render what is missing, and upload it ──\n let uploaded = 0\n let failed = 0\n const uploadedKeys: string[] = []\n /** Legacy files written this pass — only for the photograph-deleted-mid-pass cleanup. */\n const uploadedLegacyKeys: string[] = []\n const rendered = await renderPlannedVariants(\n buffer,\n toRender.map((t) => t.planned),\n )\n const work = toRender.map((t, i) => ({ ...t, variant: rendered[i] }))\n await eachBounded(work, UPLOAD_CONCURRENCY, async ({ planned, key, replace, variant }) => {\n if (!variant) return\n try {\n // An unrecorded file with no size on it predates this module's metadata\n // and cannot be trusted as the recipe's bytes — replaced, not kept.\n if (replace) await storage.delete(key)\n await storage.upload(variant.buffer, {\n key,\n filename: `${planned.shape}_${planned.width}w_${row.filename}`,\n mimeType: variant.mimeType,\n size: variant.buffer.byteLength,\n visibility: original.visibility,\n metadata: {\n variantOf: row.fileKey,\n shape: planned.shape,\n rung: planned.width,\n width: variant.width,\n height: variant.height,\n },\n })\n present.push({\n shape: planned.shape,\n rung: planned.width,\n width: variant.width,\n height: variant.height,\n format: planned.format,\n key,\n })\n uploadedKeys.push(key)\n uploaded++\n } catch (err) {\n failed++\n logger?.warn({ mediaId, key, err }, 'media variants: shaped variant upload failed')\n }\n })\n\n // ── Legacy fixed styles: re-rendered only when the recipe changed ──\n const legacy = new Map<string, string>()\n const legacyRecipes = new Map<string, string>()\n const legacyDue: Record<string, ImageStyle> = {}\n /** Styles whose old file was deleted for an in-place replacement whose upload then failed. */\n const legacyGone = new Set<string>()\n for (const [name, style] of Object.entries(styles)) {\n const key = deriveVariantKey(row.fileKey, name, style.format ?? 'webp')\n const recipe = legacyRecipe(style)\n if (before.legacy.get(name) === key && before.legacyRecipes.get(name) === recipe) {\n legacy.set(name, key)\n legacyRecipes.set(name, recipe)\n } else {\n legacyDue[name] = style\n }\n }\n if (Object.keys(legacyDue).length > 0) {\n const processed = await processImage(buffer, legacyDue)\n await eachBounded([...processed.variants], UPLOAD_CONCURRENCY, async ([name, variant]) => {\n const style = legacyDue[name]\n if (!style) return\n const key = deriveVariantKey(row.fileKey, name, variant.format)\n let oldDeleted = false\n try {\n // In place: a legacy key names no recipe, so new bytes need the old row\n // gone first. The bytes are already rendered — the gap is one upload.\n if (await storage.getMetadata(key)) {\n await storage.delete(key)\n oldDeleted = true\n }\n await storage.upload(variant.buffer, {\n key,\n filename: `${name}_${row.filename}`,\n mimeType: variant.mimeType,\n size: variant.buffer.byteLength,\n visibility: original.visibility,\n metadata: { variantOf: row.fileKey, style: name },\n })\n legacy.set(name, key)\n legacyRecipes.set(name, legacyRecipe(style))\n uploadedLegacyKeys.push(key)\n uploaded++\n } catch (err) {\n failed++\n // The old file is gone and its replacement is not there: stop pointing\n // at it, so readers fall back to the original until the retry lands.\n if (oldDeleted) legacyGone.add(name)\n logger?.warn({ mediaId, key, err }, 'media variants: legacy style upload failed')\n }\n })\n }\n\n // ── Was the plan still the right one? ──\n const fresh = await media.findById(mediaId)\n if (!fresh) {\n // Deleted while the pass ran. `MediaClient.delete` removed what was\n // recorded; what this pass wrote was never recorded, so it goes here.\n await deleteOwnedVariants(\n storage,\n row.fileKey,\n [...uploadedKeys, ...uploadedLegacyKeys],\n logger,\n )\n return result(mediaId, 'missing', { uploaded })\n }\n const stale = !sameJson(fresh.focalPoint, row.focalPoint) || !sameJson(fresh.crops, row.crops)\n\n // ── 4. Record — merged over a fresh read, so a concurrent writer's other keys survive ──\n const latest = (await storage.getMetadata(row.fileKey)) ?? original\n const latestState = readVariantState(latest.metadata)\n const mergedLegacy = without(merged(latestState.legacy, legacy), legacyGone)\n const mergedRecipes = without(merged(latestState.legacyRecipes, legacyRecipes), legacyGone)\n // A legacy style whose key CHANGED (its format did) now points at the new\n // file; the old one is recorded nowhere else. Outside a successful pass it\n // must stay findable, so it goes to `unconfirmed` (deleted by the next\n // successful pass, and with the photograph).\n const replacedLegacy = [...latestState.legacy]\n .filter(([name, key]) => mergedLegacy.has(name) && mergedLegacy.get(name) !== key)\n .map(([, key]) => key)\n const freshKeys = stale\n ? new Set(\n planVariants(drawn, config, {\n focalPoint: fresh.focalPoint,\n crops: fresh.crops,\n }).variants.map((p) => shapedKey(row.fileKey, p)),\n )\n : null\n /** Every file this pass holds that the recorded `shaped` set will not name. */\n const leftovers = (shaped: readonly StoredShapedVariant[], extra: readonly string[]) => {\n const named = new Set(shaped.map((v) => v.key))\n return [...new Set([...latestState.unconfirmed, ...extra, ...replacedLegacy])].filter(\n (k) => !named.has(k),\n )\n }\n let next: VariantState\n if (stale) {\n // The shaped set belongs to the edit's own pass. Record the legacy files\n // (independent of focal point and crops). What this pass rendered that the\n // NEW point also plans is kept as unconfirmed for the edit's pass to adopt;\n // the rest is deleted below.\n const keepForEdit = uploadedKeys.filter((k) => freshKeys?.has(k))\n next = {\n legacy: mergedLegacy,\n legacyRecipes: mergedRecipes,\n shaped: latestState.shaped,\n unconfirmed: leftovers(latestState.shaped, keepForEdit),\n }\n } else if (failed > 0) {\n // Never two crops for one (shape, rung): a variant that landed is promoted\n // only where readers have nothing for its (shape, rung) yet — so a photo\n // whose one format always fails still gets every other size. The rest is\n // UNCONFIRMED: the retry adopts it, and deleting the photograph finds it.\n const taken = new Set(latestState.shaped.map((v) => `${v.shape}@${v.rung}`))\n const promoted = present.filter((v) => !taken.has(`${v.shape}@${v.rung}`))\n const shaped = sortShaped([...latestState.shaped, ...promoted])\n next = {\n legacy: mergedLegacy,\n legacyRecipes: mergedRecipes,\n shaped,\n unconfirmed: leftovers(\n shaped,\n present.map((v) => v.key),\n ),\n }\n } else {\n next = { legacy, legacyRecipes, shaped: sortShaped(present), unconfirmed: [] }\n }\n const written = writeVariantState(latest.metadata, next)\n await storage.updateMetadata(row.fileKey, {\n metadata: seedTried ? { ...written, [FOCAL_SEED_TRIED]: true } : written,\n })\n\n // ── 5. Only now delete what the new set superseded — and only files that are\n // variants OF this original (`deleteOwnedVariants`): the lists come from\n // caller-writable metadata. ──\n const toDelete = new Set<string>()\n if (stale) {\n const stillRecorded = new Set(latestState.shaped.map((v) => v.key))\n for (const key of uploadedKeys) {\n if (!freshKeys?.has(key) && !stillRecorded.has(key)) toDelete.add(key)\n }\n } else if (failed === 0) {\n const keep = new Set([...present.map((v) => v.key), ...legacy.values()])\n for (const v of latestState.shaped) if (!keep.has(v.key)) toDelete.add(v.key)\n for (const key of latestState.legacy.values()) if (!keep.has(key)) toDelete.add(key)\n for (const key of latestState.unconfirmed) if (!keep.has(key)) toDelete.add(key)\n }\n const deleted = await deleteOwnedVariants(storage, row.fileKey, toDelete, logger)\n\n const outcome = result(mediaId, stale ? 'stale' : 'generated', {\n uploaded,\n reused,\n deleted,\n dropped: plan.dropped,\n dimensionsCorrected: 'width' in patch,\n focalPointSeeded: 'focalPoint' in patch,\n })\n if (failed > 0 && !stale) throw new VariantUploadError(mediaId, failed)\n logger?.debug(outcome, 'media variants: pass complete')\n return outcome\n}\n\nfunction merged<V>(base: ReadonlyMap<string, V>, over: ReadonlyMap<string, V>): Map<string, V> {\n return new Map([...base, ...over])\n}\n\nfunction without<V>(map: Map<string, V>, names: ReadonlySet<string>): Map<string, V> {\n for (const name of names) map.delete(name)\n return map\n}\n\n/** Shape, then width — a stable order, so an unchanged set writes unchanged JSON. */\nfunction sortShaped(list: readonly StoredShapedVariant[]): StoredShapedVariant[] {\n return [...list].sort((x, y) =>\n x.shape === y.shape ? x.rung - y.rung || x.width - y.width : x.shape < y.shape ? -1 : 1,\n )\n}\n"],"mappings":"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"}
package/dist/i18n.mjs CHANGED
@@ -1,2 +1,2 @@
1
- async function e(e){switch(e){case`et`:return(await import(`./et-CXvu8U31.mjs`)).default;case`ru`:return(await import(`./ru-Dj-ax8s_.mjs`)).default;default:return(await import(`./en-A8YzaZ51.mjs`).then(e=>e.n)).default}}export{e as getMediaMessages};
1
+ async function e(e){switch(e){case`et`:return(await import(`./et-BFdSZnb5.mjs`)).default;case`ru`:return(await import(`./ru-ojvshRJy.mjs`)).default;default:return(await import(`./en-Duj_Pp-O.mjs`).then(e=>e.n)).default}}export{e as getMediaMessages};
2
2
  //# sourceMappingURL=i18n.mjs.map
@@ -1,4 +1,4 @@
1
- import { a as ImageVocabulary, i as ImageStyle } from "./types-CgkJF5dc.mjs";
1
+ import { a as ImageVocabulary, i as ImageStyle } from "./types-6PZQ4Zq6.mjs";
2
2
 
3
3
  //#region src/image-styles-settings.d.ts
4
4
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"image-styles-settings.d.mts","names":[],"sources":["../src/image-styles-settings.ts"],"mappings":";;;;;;;;;;;;;;;;cA2Ia,kBAAA,EAAoB,MAAM,SAAS,UAAA;AAAA,cAOnC,mBAAA,kCAAmB,kBAAA"}
1
+ {"version":3,"file":"image-styles-settings.d.mts","names":[],"sources":["../src/image-styles-settings.ts"],"mappings":";;;;;;;;;;;;;;;;cA4Ia,kBAAA,EAAoB,MAAM,SAAS,UAAA;AAAA,cAOnC,mBAAA,kCAAmB,kBAAA"}
@@ -1,2 +1,2 @@
1
- import{a as e,c as t,i as n,l as r,o as i}from"./shapes-BlKW-0C1.mjs";import"./variant-plan-DV8hsMdz.mjs";import{z as a}from"zod";import{defineSettings as o,setting as s}from"@murumets-ee/settings/define";const c=a.object({width:a.number().int().positive().optional(),height:a.number().int().positive().optional(),fit:a.enum([`cover`,`contain`,`inside`,`outside`,`fill`]).optional(),format:a.enum([`webp`,`jpeg`,`png`,`avif`]).optional(),quality:a.number().int().min(1).max(100).optional()}).refine(e=>e.width!==void 0||e.height!==void 0,{message:`Style must have at least width or height`}),l=/^[a-z][a-z0-9_-]*$/,u=a.record(a.string(),c).superRefine((e,t)=>{for(let n of Object.keys(e))l.test(n)||t.addIssue({code:a.ZodIssueCode.custom,path:[n],message:`Invalid style name "${n}": must be lowercase alphanumeric (a-z, 0-9, -, _)`})}),d=a.number().int().min(16).max(n);function f(e){return e.every((t,n)=>n===0||t>(e[n-1]??-1/0))}const p=a.array(d).min(1).max(12).refine(f,{message:`Widths must be ascending, with no repeats`}),m=a.object({ratio:a.string().refine(e=>t(e)!==null,{message:`Ratio must be W:H in positive whole numbers, e.g. 3:2`}),widths:p.optional(),format:a.enum([`webp`,`jpeg`,`png`,`avif`]).optional(),quality:a.number().int().min(1).max(100).optional()}).strict(),h=a.object({shapes:a.record(a.string(),m).superRefine((t,n)=>{let r=Object.keys(t);r.length>16&&n.addIssue({code:a.ZodIssueCode.custom,message:`At most 16 shapes`});for(let t of r)(!i.test(t)||t===`natural`)&&n.addIssue({code:a.ZodIssueCode.custom,path:[t],message:`Invalid shape name "${t}": lowercase alphanumeric (a-z, 0-9, -, _), and not "${e}"`})}),ladder:p}).strict().superRefine((e,t)=>{let n=r(e);n>64&&t.addIssue({code:a.ZodIssueCode.custom,message:`These shapes and widths make ${n} files per photograph; the most is 64. Remove a shape or a width.`})}),g={thumbnail:{width:200,height:200,fit:`cover`,format:`webp`,quality:80},card:{width:600,height:400,fit:`cover`,format:`webp`,quality:85},hero:{width:1920,height:800,fit:`cover`,format:`webp`,quality:85},full:{width:2400,fit:`inside`,format:`webp`,quality:90}},_=o({namespace:`media.imageStyles`,scope:`global`,label:`Image Styles`,iconName:`image-down`,schema:{vocabulary:s.json({label:`Shapes and widths`,schema:h,renderer:`media.imageVocabulary`}),imageStyles:s.json({default:{thumbnail:g.thumbnail},label:`Image processing presets`,schema:u,renderer:`media.imageStyles`})}});export{g as defaultImageStyles,_ as imageStylesSettings};
1
+ import{a as e,c as t,i as n,l as r,o as i}from"./shapes-CqppzFzb.mjs";import"./variant-plan-86tJKtVi.mjs";import{z as a}from"zod";import{defineSettings as o,setting as s}from"@murumets-ee/settings/define";const c=a.object({width:a.number().int().positive().optional(),height:a.number().int().positive().optional(),fit:a.enum([`cover`,`contain`,`inside`,`outside`,`fill`]).optional(),format:a.enum([`webp`,`jpeg`,`png`,`avif`]).optional(),quality:a.number().int().min(1).max(100).optional()}).refine(e=>e.width!==void 0||e.height!==void 0,{message:`Style must have at least width or height`}),l=/^[a-z][a-z0-9_-]{0,63}$/,u=a.record(a.string(),c).superRefine((e,t)=>{for(let n of Object.keys(e))l.test(n)||t.addIssue({code:a.ZodIssueCode.custom,path:[n],message:`Invalid style name "${n}": must be lowercase alphanumeric (a-z, 0-9, -, _)`})}),d=a.number().int().min(16).max(n);function f(e){return e.every((t,n)=>n===0||t>(e[n-1]??-1/0))}const p=a.array(d).min(1).max(12).refine(f,{message:`Widths must be ascending, with no repeats`}),m=a.object({ratio:a.string().refine(e=>t(e)!==null,{message:`Ratio must be W:H in positive whole numbers, e.g. 3:2`}),widths:p.optional(),format:a.enum([`webp`,`jpeg`,`png`,`avif`]).optional(),quality:a.number().int().min(1).max(100).optional()}).strict(),h=a.object({shapes:a.record(a.string(),m).superRefine((t,n)=>{let r=Object.keys(t);r.length>16&&n.addIssue({code:a.ZodIssueCode.custom,message:`At most 16 shapes`});for(let t of r)(!i.test(t)||t===`natural`)&&n.addIssue({code:a.ZodIssueCode.custom,path:[t],message:`Invalid shape name "${t}": lowercase alphanumeric (a-z, 0-9, -, _), and not "${e}"`})}),ladder:p}).strict().superRefine((e,t)=>{let n=r(e);n>64&&t.addIssue({code:a.ZodIssueCode.custom,message:`These shapes and widths make ${n} files per photograph; the most is 64. Remove a shape or a width.`})}),g={thumbnail:{width:200,height:200,fit:`cover`,format:`webp`,quality:80},card:{width:600,height:400,fit:`cover`,format:`webp`,quality:85},hero:{width:1920,height:800,fit:`cover`,format:`webp`,quality:85},full:{width:2400,fit:`inside`,format:`webp`,quality:90}},_=o({namespace:`media.imageStyles`,scope:`global`,label:`Image Styles`,iconName:`image-down`,schema:{vocabulary:s.json({label:`Shapes and widths`,schema:h,renderer:`media.imageVocabulary`}),imageStyles:s.json({default:{thumbnail:g.thumbnail},label:`Image processing presets`,schema:u,renderer:`media.imageStyles`})}});export{g as defaultImageStyles,_ as imageStylesSettings};
2
2
  //# sourceMappingURL=image-styles-settings.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"image-styles-settings.mjs","names":[],"sources":["../src/image-styles-settings.ts"],"sourcesContent":["/**\n * Settings definition for media image styles.\n *\n * Stores the full Record<string, ImageStyle> as a single JSON key\n * in the toolkit_settings table under namespace 'media.imageStyles'.\n *\n * The media() plugin seeds this from config on first boot.\n * After that, the settings DB is the source of truth.\n */\n\nimport { defineSettings, setting } from '@murumets-ee/settings/define'\nimport { z } from 'zod'\nimport {\n MAX_LADDER_RUNGS,\n MAX_SHAPES,\n MAX_VARIANT_WIDTH,\n MIN_VARIANT_WIDTH,\n NATURAL_SHAPE,\n parseShapeRatio,\n plannedFileCount,\n SHAPE_NAME_PATTERN,\n} from './shapes.js'\nimport type { ImageStyle, ImageVocabulary } from './types.js'\nimport { MAX_VARIANTS_PER_IMAGE } from './variant-plan.js'\n\n/**\n * Zod schema for the `imageStyles` value (Record<styleName, ImageStyle>).\n *\n * Drives both server-side validation (settings PATCH route) and\n * client-side resolverless surfacing of field errors. The keys (style\n * names) are constrained at validation time via `.superRefine` because\n * Zod records do not support per-key regex.\n */\nconst styleSchema = z\n .object({\n width: z.number().int().positive().optional(),\n height: z.number().int().positive().optional(),\n fit: z.enum(['cover', 'contain', 'inside', 'outside', 'fill']).optional(),\n format: z.enum(['webp', 'jpeg', 'png', 'avif']).optional(),\n quality: z.number().int().min(1).max(100).optional(),\n })\n .refine((s) => s.width !== undefined || s.height !== undefined, {\n message: 'Style must have at least width or height',\n })\n\nconst STYLE_NAME_RE = /^[a-z][a-z0-9_-]*$/\n\nconst imageStylesSchema = z.record(z.string(), styleSchema).superRefine((styles, ctx) => {\n for (const name of Object.keys(styles)) {\n if (!STYLE_NAME_RE.test(name)) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n path: [name],\n message: `Invalid style name \"${name}\": must be lowercase alphanumeric (a-z, 0-9, -, _)`,\n })\n }\n }\n})\n\nconst widthSchema = z.number().int().min(MIN_VARIANT_WIDTH).max(MAX_VARIANT_WIDTH)\n\n/** Ascending and distinct — the order a `srcset` and the generator both assume. */\nfunction isAscendingDistinct(widths: readonly number[]): boolean {\n return widths.every((w, i) => i === 0 || w > (widths[i - 1] ?? Number.NEGATIVE_INFINITY))\n}\n\nconst widthListSchema = z\n .array(widthSchema)\n .min(1)\n .max(MAX_LADDER_RUNGS)\n .refine(isAscendingDistinct, { message: 'Widths must be ascending, with no repeats' })\n\nconst shapeSchema = z\n .object({\n ratio: z.string().refine((r) => parseShapeRatio(r) !== null, {\n message: 'Ratio must be W:H in positive whole numbers, e.g. 3:2',\n }),\n widths: widthListSchema.optional(),\n format: z.enum(['webp', 'jpeg', 'png', 'avif']).optional(),\n quality: z.number().int().min(1).max(100).optional(),\n })\n .strict()\n\n/**\n * Zod schema for the `vocabulary` value — the shape vocabulary and the width\n * ladder (images D003). `.strict()` so a stored value carries nothing the\n * generator does not read.\n */\nconst vocabularySchema = z\n .object({\n shapes: z.record(z.string(), shapeSchema).superRefine((shapes, ctx) => {\n const names = Object.keys(shapes)\n if (names.length > MAX_SHAPES) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n message: `At most ${MAX_SHAPES} shapes`,\n })\n }\n for (const name of names) {\n if (!SHAPE_NAME_PATTERN.test(name) || name === NATURAL_SHAPE) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n path: [name],\n message: `Invalid shape name \"${name}\": lowercase alphanumeric (a-z, 0-9, -, _), and not \"${NATURAL_SHAPE}\"`,\n })\n }\n }\n }),\n ladder: widthListSchema,\n })\n .strict()\n .superRefine((vocabulary, ctx) => {\n // Every (shape × width) is a file per photograph. Refuse a vocabulary that\n // on its OWN plans more than the per-image cap, rather than store it and\n // cut it on every upload. `natural` counts: it runs over the ladder too.\n // The fixed styles are a separate key and add one each on top; the\n // planner trims those (largest first) and `resolveVariantConfig` warns.\n const planned = plannedFileCount(vocabulary)\n if (planned > MAX_VARIANTS_PER_IMAGE) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n message: `These shapes and widths make ${planned} files per photograph; the most is ${MAX_VARIANTS_PER_IMAGE}. Remove a shape or a width.`,\n })\n }\n })\n\n/**\n * Curated default image style presets for new projects.\n *\n * Consumers can register all of these via the media plugin config:\n * media({ imageStyles: defaultImageStyles })\n *\n * Or pick a subset:\n * media({ imageStyles: { thumbnail: defaultImageStyles.thumbnail } })\n *\n * The `imageStylesSettings` default below intentionally only includes\n * `thumbnail` — adding more here would auto-create them for every project\n * that uses the plugin without opting in.\n */\nexport const defaultImageStyles: Record<string, ImageStyle> = {\n thumbnail: { width: 200, height: 200, fit: 'cover', format: 'webp', quality: 80 },\n card: { width: 600, height: 400, fit: 'cover', format: 'webp', quality: 85 },\n hero: { width: 1920, height: 800, fit: 'cover', format: 'webp', quality: 85 },\n full: { width: 2400, fit: 'inside', format: 'webp', quality: 90 },\n}\n\nexport const imageStylesSettings = defineSettings({\n namespace: 'media.imageStyles',\n scope: 'global',\n label: 'Image Styles',\n iconName: 'image-down',\n schema: {\n /**\n * The shape vocabulary and width ladder (images D003). Deliberately NO\n * `default`: an unset key reads as `null`, which is what lets the waterfall\n * reach the plugin config (`media({ vocabulary })`) and then the seeded\n * `DEFAULT_IMAGE_VOCABULARY` — see `resolveVariantConfig`.\n */\n vocabulary: setting.json<ImageVocabulary>({\n label: 'Shapes and widths',\n schema: vocabularySchema,\n renderer: 'media.imageVocabulary',\n }),\n imageStyles: setting.json<Record<string, ImageStyle>>({\n default: {\n thumbnail: defaultImageStyles.thumbnail,\n },\n label: 'Image processing presets',\n schema: imageStylesSchema,\n renderer: 'media.imageStyles',\n }),\n },\n})\n"],"mappings":"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"}
1
+ {"version":3,"file":"image-styles-settings.mjs","names":[],"sources":["../src/image-styles-settings.ts"],"sourcesContent":["/**\n * Settings definition for media image styles.\n *\n * Stores the full Record<string, ImageStyle> as a single JSON key\n * in the toolkit_settings table under namespace 'media.imageStyles'.\n *\n * The media() plugin seeds this from config on first boot.\n * After that, the settings DB is the source of truth.\n */\n\nimport { defineSettings, setting } from '@murumets-ee/settings/define'\nimport { z } from 'zod'\nimport {\n MAX_LADDER_RUNGS,\n MAX_SHAPES,\n MAX_VARIANT_WIDTH,\n MIN_VARIANT_WIDTH,\n NATURAL_SHAPE,\n parseShapeRatio,\n plannedFileCount,\n SHAPE_NAME_PATTERN,\n} from './shapes.js'\nimport type { ImageStyle, ImageVocabulary } from './types.js'\nimport { MAX_VARIANTS_PER_IMAGE } from './variant-plan.js'\n\n/**\n * Zod schema for the `imageStyles` value (Record<styleName, ImageStyle>).\n *\n * Drives both server-side validation (settings PATCH route) and\n * client-side resolverless surfacing of field errors. The keys (style\n * names) are constrained at validation time via `.superRefine` because\n * Zod records do not support per-key regex.\n */\nconst styleSchema = z\n .object({\n width: z.number().int().positive().optional(),\n height: z.number().int().positive().optional(),\n fit: z.enum(['cover', 'contain', 'inside', 'outside', 'fill']).optional(),\n format: z.enum(['webp', 'jpeg', 'png', 'avif']).optional(),\n quality: z.number().int().min(1).max(100).optional(),\n })\n .refine((s) => s.width !== undefined || s.height !== undefined, {\n message: 'Style must have at least width or height',\n })\n\n// Bounded at 64 like `SHAPE_NAME_PATTERN`: an edited style is a shape, and its name a crops key.\nconst STYLE_NAME_RE = /^[a-z][a-z0-9_-]{0,63}$/\n\nconst imageStylesSchema = z.record(z.string(), styleSchema).superRefine((styles, ctx) => {\n for (const name of Object.keys(styles)) {\n if (!STYLE_NAME_RE.test(name)) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n path: [name],\n message: `Invalid style name \"${name}\": must be lowercase alphanumeric (a-z, 0-9, -, _)`,\n })\n }\n }\n})\n\nconst widthSchema = z.number().int().min(MIN_VARIANT_WIDTH).max(MAX_VARIANT_WIDTH)\n\n/** Ascending and distinct — the order a `srcset` and the generator both assume. */\nfunction isAscendingDistinct(widths: readonly number[]): boolean {\n return widths.every((w, i) => i === 0 || w > (widths[i - 1] ?? Number.NEGATIVE_INFINITY))\n}\n\nconst widthListSchema = z\n .array(widthSchema)\n .min(1)\n .max(MAX_LADDER_RUNGS)\n .refine(isAscendingDistinct, { message: 'Widths must be ascending, with no repeats' })\n\nconst shapeSchema = z\n .object({\n ratio: z.string().refine((r) => parseShapeRatio(r) !== null, {\n message: 'Ratio must be W:H in positive whole numbers, e.g. 3:2',\n }),\n widths: widthListSchema.optional(),\n format: z.enum(['webp', 'jpeg', 'png', 'avif']).optional(),\n quality: z.number().int().min(1).max(100).optional(),\n })\n .strict()\n\n/**\n * Zod schema for the `vocabulary` value — the shape vocabulary and the width\n * ladder (images D003). `.strict()` so a stored value carries nothing the\n * generator does not read.\n */\nconst vocabularySchema = z\n .object({\n shapes: z.record(z.string(), shapeSchema).superRefine((shapes, ctx) => {\n const names = Object.keys(shapes)\n if (names.length > MAX_SHAPES) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n message: `At most ${MAX_SHAPES} shapes`,\n })\n }\n for (const name of names) {\n if (!SHAPE_NAME_PATTERN.test(name) || name === NATURAL_SHAPE) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n path: [name],\n message: `Invalid shape name \"${name}\": lowercase alphanumeric (a-z, 0-9, -, _), and not \"${NATURAL_SHAPE}\"`,\n })\n }\n }\n }),\n ladder: widthListSchema,\n })\n .strict()\n .superRefine((vocabulary, ctx) => {\n // Every (shape × width) is a file per photograph. Refuse a vocabulary that\n // on its OWN plans more than the per-image cap, rather than store it and\n // cut it on every upload. `natural` counts: it runs over the ladder too.\n // The fixed styles are a separate key and add one each on top; the\n // planner trims those (largest first) and `resolveVariantConfig` warns.\n const planned = plannedFileCount(vocabulary)\n if (planned > MAX_VARIANTS_PER_IMAGE) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n message: `These shapes and widths make ${planned} files per photograph; the most is ${MAX_VARIANTS_PER_IMAGE}. Remove a shape or a width.`,\n })\n }\n })\n\n/**\n * Curated default image style presets for new projects.\n *\n * Consumers can register all of these via the media plugin config:\n * media({ imageStyles: defaultImageStyles })\n *\n * Or pick a subset:\n * media({ imageStyles: { thumbnail: defaultImageStyles.thumbnail } })\n *\n * The `imageStylesSettings` default below intentionally only includes\n * `thumbnail` — adding more here would auto-create them for every project\n * that uses the plugin without opting in.\n */\nexport const defaultImageStyles: Record<string, ImageStyle> = {\n thumbnail: { width: 200, height: 200, fit: 'cover', format: 'webp', quality: 80 },\n card: { width: 600, height: 400, fit: 'cover', format: 'webp', quality: 85 },\n hero: { width: 1920, height: 800, fit: 'cover', format: 'webp', quality: 85 },\n full: { width: 2400, fit: 'inside', format: 'webp', quality: 90 },\n}\n\nexport const imageStylesSettings = defineSettings({\n namespace: 'media.imageStyles',\n scope: 'global',\n label: 'Image Styles',\n iconName: 'image-down',\n schema: {\n /**\n * The shape vocabulary and width ladder (images D003). Deliberately NO\n * `default`: an unset key reads as `null`, which is what lets the waterfall\n * reach the plugin config (`media({ vocabulary })`) and then the seeded\n * `DEFAULT_IMAGE_VOCABULARY` — see `resolveVariantConfig`.\n */\n vocabulary: setting.json<ImageVocabulary>({\n label: 'Shapes and widths',\n schema: vocabularySchema,\n renderer: 'media.imageVocabulary',\n }),\n imageStyles: setting.json<Record<string, ImageStyle>>({\n default: {\n thumbnail: defaultImageStyles.thumbnail,\n },\n label: 'Image processing presets',\n schema: imageStylesSchema,\n renderer: 'media.imageStyles',\n }),\n },\n})\n"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import { a as ImageVocabulary, i as ImageStyle } from "./types-CgkJF5dc.mjs";
1
+ import { a as ImageVocabulary, i as ImageStyle } from "./types-6PZQ4Zq6.mjs";
2
2
 
3
3
  //#region src/image-styles/types.d.ts
4
4
  interface ImageStylesManagerLabels {
@@ -1,2 +1,2 @@
1
- "use client";import{c as e,i as t,l as n,o as r,r as i}from"./shapes-BlKW-0C1.mjs";import"./variant-plan-DV8hsMdz.mjs";import{Loader2 as a,Pencil as o,Plus as s,RefreshCw as c,Trash2 as l}from"lucide-react";import{Fragment as u,jsx as d,jsxs as f}from"react/jsx-runtime";import{useCallback as p,useId as m,useRef as h,useState as g}from"react";import{Badge as _,Button as v,Dialog as y,DialogContent as b,DialogDescription as x,DialogFooter as S,DialogHeader as C,DialogTitle as w,Input as T,Label as E,Select as D,Table as O,TableBody as k,TableCell as A,TableHead as j,TableHeader as M,TableRow as N,cn as P}from"@murumets-ee/ui";import{useTranslations as F}from"next-intl";const I=[`cover`,`contain`,`inside`,`outside`,`fill`],L=[`webp`,`jpeg`,`png`,`avif`],ee={title:`Image Styles`,description:`Configure image processing presets. Every image gets a file for each style, generated in the background by the queue worker. Save the form to apply changes.`,addStyle:`Add Style`,editStyle:`Edit Style`,styleName:`Style Name`,width:`Width`,height:`Height`,fit:`Fit`,format:`Format`,quality:`Quality`,save:`Save`,cancel:`Cancel`,delete:`Delete`,deleteConfirmTitle:`Delete Image Style`,deleteConfirmDescription:`Are you sure? This will not remove existing variant files.`,systemBadge:`system`,noStyles:`No image styles configured.`,px:`px`};function R({value:e,onChange:t,error:n,labels:r,classNames:i}){let a={...ee,...r},c=e??{},[u,m]=g(!1),[h,F]=g(null),[R,z]=g(``),[B,V]=g(``),[H,U]=g(``),[W,G]=g(`cover`),[K,q]=g(`webp`),[J,Y]=g(`80`),[X,Z]=g(null),[te,Q]=g(!1),[$,ne]=g(null),re=p(()=>{F(null),z(``),V(``),U(``),G(`cover`),q(`webp`),Y(`80`),Z(null),m(!0)},[]),ie=p(e=>{let t=c[e];t&&(F(e),z(e),V(t.width?.toString()??``),U(t.height?.toString()??``),G(t.fit??`cover`),q(t.format??`webp`),Y((t.quality??80).toString()),Z(null),m(!0))},[c]),ae=p(()=>{Z(null);let e=R.trim().toLowerCase();if(!e||!/^[a-z][a-z0-9_-]*$/.test(e)){Z(`Name must be lowercase alphanumeric (a-z, 0-9, -, _)`);return}if(e!==h&&c[e]){Z(`A style named "${e}" already exists`);return}let n=B?Number.parseInt(B,10):void 0,r=H?Number.parseInt(H,10):void 0;if(!n&&!r){Z(`At least width or height is required`);return}if(n!==void 0&&(Number.isNaN(n)||n<=0)){Z(`Width must be a positive number`);return}if(r!==void 0&&(Number.isNaN(r)||r<=0)){Z(`Height must be a positive number`);return}let i=Number.parseInt(J,10);if(Number.isNaN(i)||i<1||i>100){Z(`Quality must be 1-100`);return}let a=W,o=K,s={...n?{width:n}:{},...r?{height:r}:{},fit:a,format:o,quality:i},l={...c};h&&h!==e&&delete l[h],l[e]=s,t(l),m(!1)},[c,h,R,B,H,W,K,J,t]),oe=p(e=>{ne(e),Q(!0)},[]),se=p(()=>{if(!$)return;let e={...c};delete e[$],t(e),Q(!1),ne(null)},[$,c,t]),ce=Object.entries(c).sort(([e],[t])=>e.localeCompare(t));return f(`div`,{className:P(`space-y-4`,i?.root),children:[f(`div`,{className:P(`flex items-start justify-between`,i?.header),children:[d(`p`,{className:`text-sm text-muted-foreground`,children:a.description}),f(v,{onClick:re,size:`sm`,type:`button`,children:[d(s,{className:`h-4 w-4 mr-1`}),a.addStyle]})]}),n&&d(`div`,{className:`rounded-md border border-destructive/50 bg-destructive/10 px-4 py-3 text-sm text-destructive`,children:n}),ce.length===0?d(`p`,{className:`text-sm text-muted-foreground py-8 text-center`,children:a.noStyles}):d(`div`,{className:P(`rounded-md border`,i?.table),children:f(O,{children:[d(M,{children:f(N,{children:[d(j,{children:a.styleName}),d(j,{className:`text-center`,children:a.width}),d(j,{className:`text-center`,children:a.height}),d(j,{className:`text-center`,children:a.fit}),d(j,{className:`text-center`,children:a.format}),d(j,{className:`text-center`,children:a.quality}),d(j,{className:`w-24`})]})}),d(k,{children:ce.map(([e,t])=>f(N,{children:[f(A,{className:`font-mono text-sm`,children:[e,e===`thumbnail`&&d(_,{variant:`secondary`,className:`ml-2 text-xs`,children:a.systemBadge})]}),d(A,{className:`text-center tabular-nums`,children:t.width?`${t.width}${a.px}`:`—`}),d(A,{className:`text-center tabular-nums`,children:t.height?`${t.height}${a.px}`:`—`}),d(A,{className:`text-center text-sm text-muted-foreground`,children:t.fit??`cover`}),d(A,{className:`text-center text-sm text-muted-foreground`,children:t.format??`webp`}),d(A,{className:`text-center tabular-nums`,children:t.quality??80}),d(A,{children:f(`div`,{className:P(`flex items-center gap-1 justify-end`,i?.actions),children:[d(v,{variant:`ghost`,size:`sm`,type:`button`,onClick:()=>ie(e),title:a.editStyle,children:d(o,{className:`h-3.5 w-3.5`})}),d(v,{variant:`ghost`,size:`sm`,type:`button`,onClick:()=>oe(e),title:a.delete,className:`text-destructive hover:text-destructive`,children:d(l,{className:`h-3.5 w-3.5`})})]})})]},e))})]})}),d(y,{open:u,onOpenChange:m,children:f(b,{className:i?.dialog,children:[f(C,{children:[d(w,{children:h?a.editStyle:a.addStyle}),d(x,{children:h?`Editing "${h}" image style.`:`Create a new image processing preset.`})]}),f(`div`,{className:`space-y-4 py-2`,children:[X&&d(`div`,{className:`text-sm text-destructive`,children:X}),f(`div`,{className:`space-y-2`,children:[d(E,{htmlFor:`style-name`,children:a.styleName}),d(T,{id:`style-name`,value:R,onChange:e=>z(e.target.value),placeholder:`e.g. thumbnail, medium, large`,disabled:!!h})]}),f(`div`,{className:`grid grid-cols-2 gap-4`,children:[f(`div`,{className:`space-y-2`,children:[f(E,{htmlFor:`style-width`,children:[a.width,` (`,a.px,`)`]}),d(T,{id:`style-width`,type:`number`,value:B,onChange:e=>V(e.target.value),placeholder:`e.g. 200`,min:1})]}),f(`div`,{className:`space-y-2`,children:[f(E,{htmlFor:`style-height`,children:[a.height,` (`,a.px,`)`]}),d(T,{id:`style-height`,type:`number`,value:H,onChange:e=>U(e.target.value),placeholder:`e.g. 200`,min:1})]})]}),f(`div`,{className:`grid grid-cols-2 gap-4`,children:[f(`div`,{className:`space-y-2`,children:[d(E,{htmlFor:`style-fit`,children:a.fit}),d(D,{id:`style-fit`,value:W,onChange:e=>G(e.target.value),children:I.map(e=>d(`option`,{value:e,children:e},e))})]}),f(`div`,{className:`space-y-2`,children:[d(E,{htmlFor:`style-format`,children:a.format}),d(D,{id:`style-format`,value:K,onChange:e=>q(e.target.value),children:L.map(e=>d(`option`,{value:e,children:e},e))})]})]}),f(`div`,{className:`space-y-2`,children:[f(E,{htmlFor:`style-quality`,children:[a.quality,` (1-100)`]}),d(T,{id:`style-quality`,type:`number`,value:J,onChange:e=>Y(e.target.value),min:1,max:100})]})]}),f(S,{children:[d(v,{variant:`outline`,type:`button`,onClick:()=>m(!1),children:a.cancel}),d(v,{type:`button`,onClick:ae,children:a.save})]})]})}),d(y,{open:te,onOpenChange:Q,children:f(b,{children:[f(C,{children:[d(w,{children:a.deleteConfirmTitle}),f(x,{children:[a.deleteConfirmDescription,$&&d(`span`,{className:`block mt-2 font-mono text-foreground`,children:$})]})]}),f(S,{children:[d(v,{variant:`outline`,type:`button`,onClick:()=>Q(!1),children:a.cancel}),d(v,{variant:`destructive`,type:`button`,onClick:se,children:a.delete})]})]})})]})}const z={description:[`Media.imageVocabulary.description`,`Every photograph is generated in each shape below, at each width. A slot that names a shape draws these files; a slot with its own ratio is cropped in the browser instead.`],usingDefaults:[`Media.imageVocabulary.usingDefaults`,`Nothing is saved here yet. Shown are the toolkit defaults; until you save, the site uses them — or the shapes configured in its code, if it has any.`],widthsLabel:[`Media.imageVocabulary.widthsLabel`,`Widths (px)`],widthsHelp:[`Media.imageVocabulary.widthsHelp`,`Ascending, separated by commas. A photograph is never enlarged — a width it cannot supply is skipped.`],widthsInvalid:[`Media.imageVocabulary.widthsInvalid`,`Use up to {count} ascending whole numbers between {min} and {max}.`],shapesLabel:[`Media.imageVocabulary.shapesLabel`,`Shapes`],naturalNote:[`Media.imageVocabulary.naturalNote`,`Plus “natural” — each photograph at its own shape, over the same widths.`],columnName:[`Media.imageVocabulary.columnName`,`Name`],columnRatio:[`Media.imageVocabulary.columnRatio`,`Ratio (W:H)`],columnWidths:[`Media.imageVocabulary.columnWidths`,`Own widths`],ownWidthsPlaceholder:[`Media.imageVocabulary.ownWidthsPlaceholder`,`All widths`],ratioInvalid:[`Media.imageVocabulary.ratioInvalid`,`Write the ratio as W:H, for example 3:2.`],nameInvalid:[`Media.imageVocabulary.nameInvalid`,`Lowercase letters, digits, - and _, starting with a letter — and not “natural”.`],nameTaken:[`Media.imageVocabulary.nameTaken`,`A shape named “{name}” already exists.`],tooMany:[`Media.imageVocabulary.tooMany`,`At most {count} shapes.`],tooManyFiles:[`Media.imageVocabulary.tooManyFiles`,`That makes {count} files for every photograph; the most is {max}. Remove a shape or a width first.`],add:[`Media.imageVocabulary.add`,`Add shape`],remove:[`Media.imageVocabulary.remove`,`Remove {name}`]},B=Object.values(z).map(([e])=>e);function V(e,t){return t?e.replace(/\{(\w+)\}/g,(e,n)=>n in t?String(t[n]):e):e}function H(){let e=F();return(t,n)=>{let[r,i]=z[t];return e.has(r)?e(r,n):V(i,n)}}function U(e){let t=e.split(`,`).map(e=>e.trim()).filter(e=>e!==``);if(t.length===0||t.length>12)return null;let n=[];for(let e of t){if(!/^\d+$/.test(e))return null;let t=Number(e);if(t<16||t>8192)return null;let r=n[n.length-1];if(r!==void 0&&t<=r)return null;n.push(t)}return n}function W(e,t){return e.length===t.length&&e.every((e,n)=>e===t[n])}function G(e){return e?e.join(`, `):``}function K(e){return t=>{t.key!==`Enter`||t.nativeEvent.isComposing||t.keyCode===229||(t.preventDefault(),e())}}function q(){return{shapes:Object.fromEntries(Object.entries(i.shapes).map(([e,t])=>[e,{...t}])),ladder:[...i.ladder]}}function J(e){let t=JSON.stringify(e.value??null),[n,r]=g(null),[i,a]=g(t),[o,s]=g(0);t!==i&&(a(t),t!==n&&s(o+1));let c=t=>{r(JSON.stringify(t)),e.onChange(t)};return d(Y,{...e,onChange:c},o)}function Y({value:i,onChange:a,error:o,className:c}){let l=H(),u=m(),p=i??q(),_=Object.keys(p.shapes).sort(),[y,b]=g(G(p.ladder)),[x,S]=g(!1),[C,w]=g(``),[D,O]=g(``),[k,A]=g(null),[j,M]=g(null),N=h(null),F=e=>{let t=n(e);return t>64?(M(l(`tooManyFiles`,{count:t,max:64})),!1):(M(null),a(e),!0)},I=()=>{let e=U(y);S(e===null),e&&!W(e,p.ladder)&&F({...p,ladder:e})},L=()=>{let t=C.trim(),n=D.trim();if(!r.test(t)||t===`natural`){A(l(`nameInvalid`));return}if(Object.hasOwn(p.shapes,t)){A(l(`nameTaken`,{name:t}));return}if(_.length>=16){A(l(`tooMany`,{count:16}));return}if(e(n)===null){A(l(`ratioInvalid`));return}A(null),F({...p,shapes:{...p.shapes,[t]:{ratio:n}}})&&(w(``),O(``))},ee=(e,t)=>F({...p,shapes:{...p.shapes,[e]:t}}),R=e=>{let{[e]:t,...n}=p.shapes;F({...p,shapes:n})};return f(`div`,{className:P(`space-y-6`,c),children:[f(`div`,{className:`space-y-1 text-sm text-muted-foreground`,children:[d(`p`,{children:l(`description`)}),i==null?d(`p`,{children:l(`usingDefaults`)}):null]}),j?d(`p`,{role:`alert`,className:`text-sm text-destructive`,children:j}):null,o?d(`div`,{className:`rounded-md border border-destructive/50 bg-destructive/10 px-4 py-3 text-sm text-destructive`,children:o}):null,f(`div`,{className:`space-y-2`,children:[d(E,{htmlFor:`${u}-ladder`,children:l(`widthsLabel`)}),d(T,{id:`${u}-ladder`,value:y,onChange:e=>b(e.target.value),onBlur:I,onKeyDown:K(I),"aria-invalid":x,"aria-describedby":`${u}-ladder-help`,inputMode:`numeric`}),d(`p`,{id:`${u}-ladder-help`,className:P(`text-xs`,x?`text-destructive`:`text-muted-foreground`),children:x?l(`widthsInvalid`,{count:12,min:16,max:t}):l(`widthsHelp`)})]}),f(`fieldset`,{className:`space-y-2`,children:[d(`legend`,{className:`text-sm font-medium`,children:l(`shapesLabel`)}),f(`div`,{className:`grid grid-cols-[minmax(6rem,1fr)_8rem_minmax(8rem,2fr)_2.5rem] gap-2 text-xs text-muted-foreground`,children:[d(`span`,{children:l(`columnName`)}),d(`span`,{children:l(`columnRatio`)}),d(`span`,{children:l(`columnWidths`)}),d(`span`,{})]}),_.map(e=>{let t=p.shapes[e];return t?d(X,{name:e,shape:t,copy:l,onReplace:t=>ee(e,t),onRemove:()=>R(e)},e):null}),d(`p`,{className:`text-xs text-muted-foreground`,children:l(`naturalNote`)}),f(`div`,{className:`grid grid-cols-[minmax(6rem,1fr)_8rem_auto] items-start gap-2 pt-2`,children:[d(T,{"aria-label":l(`columnName`),value:C,onChange:e=>w(e.target.value),onKeyDown:K(()=>N.current?.focus())}),d(T,{ref:N,"aria-label":l(`columnRatio`),value:D,onChange:e=>O(e.target.value),onKeyDown:K(L),placeholder:`3:2`}),f(v,{type:`button`,size:`sm`,onClick:L,children:[d(s,{className:`mr-1 h-4 w-4`}),l(`add`)]})]}),k?d(`p`,{role:`alert`,className:`text-xs text-destructive`,children:k}):null]})]})}function X({name:n,shape:r,copy:i,onReplace:a,onRemove:o}){let[s,c]=g(r.ratio),[u,p]=g(G(r.widths)),[m,h]=g(null),_=()=>{let t=e(s.trim())!==null;h(t?null:`ratioInvalid`),t&&s.trim()!==r.ratio&&a({...r,ratio:s.trim()})},y=()=>{if(u.trim()===``){h(null);let{widths:e,...t}=r;r.widths&&a(t);return}let e=U(u);h(e?null:`widthsInvalid`),e&&!W(e,r.widths??[])&&a({...r,widths:e})};return f(`div`,{className:`space-y-1`,children:[f(`div`,{className:`grid grid-cols-[minmax(6rem,1fr)_8rem_minmax(8rem,2fr)_2.5rem] items-center gap-2`,children:[d(`span`,{className:`font-mono text-sm`,children:n}),d(T,{"aria-label":`${n} ${i(`columnRatio`)}`,value:s,onChange:e=>c(e.target.value),onBlur:_,onKeyDown:K(_),"aria-invalid":m===`ratioInvalid`}),d(T,{"aria-label":`${n} ${i(`columnWidths`)}`,value:u,onChange:e=>p(e.target.value),onBlur:y,onKeyDown:K(y),placeholder:i(`ownWidthsPlaceholder`),"aria-invalid":m===`widthsInvalid`}),d(v,{type:`button`,variant:`ghost`,size:`sm`,onClick:o,title:i(`remove`,{name:n}),"aria-label":i(`remove`,{name:n}),className:`text-destructive hover:text-destructive`,children:d(l,{className:`h-3.5 w-3.5`})})]}),m?d(`p`,{role:`alert`,className:`text-xs text-destructive`,children:i(m,{count:12,min:16,max:t})}):null]})}const Z={workerRequired:[`Media.variantGeneration.workerRequired`,`Image sizes are generated in the background by the queue worker — a separate process that has to run beside the site (pnpm worker). Without it, the site still works, but new and changed images are shown at full size.`],regenerate:[`Media.variantGeneration.regenerate`,`Catch up all images`],regenerating:[`Media.variantGeneration.regenerating`,`Starting…`],queued:[`Media.variantGeneration.queued`,`Started. The queue worker catches the library up in the background; the site keeps serving meanwhile. Follow it on the Queue page.`],alreadyRunning:[`Media.variantGeneration.alreadyRunning`,`A catch-up is already running — follow it on the Queue page.`],inlineDone:[`Media.variantGeneration.inlineDone`,`Processed {processed} of {total}; skipped {skipped}; errors {errors}.`],failed:[`Media.variantGeneration.failed`,`Could not start the catch-up: {reason}`]};Object.values(Z).map(([e])=>e);function te(e,t){return t?e.replace(/\{(\w+)\}/g,(e,n)=>n in t?String(t[n]):e):e}function Q({apiBasePath:e,labels:t}){let n=F(),r=(e,t)=>{let[r,i]=Z[e];return n.has(r)?n(r,t):te(i,t)},[i,o]=g(!1),[s,l]=g(null),m=p(async()=>{o(!0),l(null);try{let t=await fetch(`${e}/media/regenerate-variants`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({})});if(!t.ok){l({kind:`error`,reason:(await t.json().catch(()=>({}))).error??String(t.status)});return}let n=await t.json();l(n.mode===`queued`?{kind:`queued`,alreadyRunning:n.alreadyRunning}:{kind:`inline`,...n})}catch(e){l({kind:`error`,reason:e instanceof Error?e.message:String(e)})}finally{o(!1)}},[e]);return f(`div`,{className:`flex max-w-xl flex-col items-end gap-2`,children:[d(`p`,{className:`text-right text-xs text-muted-foreground`,"data-variant-worker-required":``,children:r(`workerRequired`)}),f(`div`,{className:`flex items-center gap-3`,children:[s?.kind===`queued`&&d(`span`,{className:`text-xs text-muted-foreground`,role:`status`,children:r(s.alreadyRunning?`alreadyRunning`:`queued`)}),s?.kind===`inline`&&d(`span`,{className:`text-xs text-muted-foreground`,role:`status`,children:r(`inlineDone`,{processed:s.processed,total:s.total,skipped:s.skipped,errors:s.errors})}),s?.kind===`error`&&d(`span`,{className:`text-xs text-destructive`,role:`alert`,children:r(`failed`,{reason:s.reason})}),d(v,{variant:`outline`,size:`sm`,type:`button`,onClick:m,disabled:i,children:i?f(u,{children:[d(a,{className:`h-4 w-4 mr-1 animate-spin`}),t?.regenerating??r(`regenerating`)]}):f(u,{children:[d(c,{className:`h-4 w-4 mr-1`}),t?.regenerate??r(`regenerate`)]})})]})]})}export{B as IMAGE_VOCABULARY_MESSAGE_KEYS,R as ImageStylesManager,J as ImageVocabularyEditor,Q as RegenerateVariantsAction};
1
+ "use client";import{c as e,i as t,l as n,o as r,r as i}from"./shapes-CqppzFzb.mjs";import"./variant-plan-86tJKtVi.mjs";import{Loader2 as a,Pencil as o,Plus as s,RefreshCw as c,Trash2 as l}from"lucide-react";import{Fragment as u,jsx as d,jsxs as f}from"react/jsx-runtime";import{useCallback as p,useId as m,useRef as h,useState as g}from"react";import{Badge as _,Button as v,Dialog as y,DialogContent as b,DialogDescription as x,DialogFooter as S,DialogHeader as C,DialogTitle as w,Input as T,Label as E,Select as D,Table as O,TableBody as k,TableCell as A,TableHead as j,TableHeader as M,TableRow as N,cn as P}from"@murumets-ee/ui";import{useTranslations as F}from"next-intl";const I=[`cover`,`contain`,`inside`,`outside`,`fill`],L=[`webp`,`jpeg`,`png`,`avif`],ee={title:`Image Styles`,description:`Configure image processing presets. Every image gets a file for each style, generated in the background by the queue worker. Save the form to apply changes.`,addStyle:`Add Style`,editStyle:`Edit Style`,styleName:`Style Name`,width:`Width`,height:`Height`,fit:`Fit`,format:`Format`,quality:`Quality`,save:`Save`,cancel:`Cancel`,delete:`Delete`,deleteConfirmTitle:`Delete Image Style`,deleteConfirmDescription:`Are you sure? This will not remove existing variant files.`,systemBadge:`system`,noStyles:`No image styles configured.`,px:`px`};function R({value:e,onChange:t,error:n,labels:r,classNames:i}){let a={...ee,...r},c=e??{},[u,m]=g(!1),[h,F]=g(null),[R,z]=g(``),[B,V]=g(``),[H,U]=g(``),[W,G]=g(`cover`),[K,q]=g(`webp`),[J,Y]=g(`80`),[X,Z]=g(null),[te,Q]=g(!1),[$,ne]=g(null),re=p(()=>{F(null),z(``),V(``),U(``),G(`cover`),q(`webp`),Y(`80`),Z(null),m(!0)},[]),ie=p(e=>{let t=c[e];t&&(F(e),z(e),V(t.width?.toString()??``),U(t.height?.toString()??``),G(t.fit??`cover`),q(t.format??`webp`),Y((t.quality??80).toString()),Z(null),m(!0))},[c]),ae=p(()=>{Z(null);let e=R.trim().toLowerCase();if(!e||!/^[a-z][a-z0-9_-]*$/.test(e)){Z(`Name must be lowercase alphanumeric (a-z, 0-9, -, _)`);return}if(e!==h&&c[e]){Z(`A style named "${e}" already exists`);return}let n=B?Number.parseInt(B,10):void 0,r=H?Number.parseInt(H,10):void 0;if(!n&&!r){Z(`At least width or height is required`);return}if(n!==void 0&&(Number.isNaN(n)||n<=0)){Z(`Width must be a positive number`);return}if(r!==void 0&&(Number.isNaN(r)||r<=0)){Z(`Height must be a positive number`);return}let i=Number.parseInt(J,10);if(Number.isNaN(i)||i<1||i>100){Z(`Quality must be 1-100`);return}let a=W,o=K,s={...n?{width:n}:{},...r?{height:r}:{},fit:a,format:o,quality:i},l={...c};h&&h!==e&&delete l[h],l[e]=s,t(l),m(!1)},[c,h,R,B,H,W,K,J,t]),oe=p(e=>{ne(e),Q(!0)},[]),se=p(()=>{if(!$)return;let e={...c};delete e[$],t(e),Q(!1),ne(null)},[$,c,t]),ce=Object.entries(c).sort(([e],[t])=>e.localeCompare(t));return f(`div`,{className:P(`space-y-4`,i?.root),children:[f(`div`,{className:P(`flex items-start justify-between`,i?.header),children:[d(`p`,{className:`text-sm text-muted-foreground`,children:a.description}),f(v,{onClick:re,size:`sm`,type:`button`,children:[d(s,{className:`h-4 w-4 mr-1`}),a.addStyle]})]}),n&&d(`div`,{className:`rounded-md border border-destructive/50 bg-destructive/10 px-4 py-3 text-sm text-destructive`,children:n}),ce.length===0?d(`p`,{className:`text-sm text-muted-foreground py-8 text-center`,children:a.noStyles}):d(`div`,{className:P(`rounded-md border`,i?.table),children:f(O,{children:[d(M,{children:f(N,{children:[d(j,{children:a.styleName}),d(j,{className:`text-center`,children:a.width}),d(j,{className:`text-center`,children:a.height}),d(j,{className:`text-center`,children:a.fit}),d(j,{className:`text-center`,children:a.format}),d(j,{className:`text-center`,children:a.quality}),d(j,{className:`w-24`})]})}),d(k,{children:ce.map(([e,t])=>f(N,{children:[f(A,{className:`font-mono text-sm`,children:[e,e===`thumbnail`&&d(_,{variant:`secondary`,className:`ml-2 text-xs`,children:a.systemBadge})]}),d(A,{className:`text-center tabular-nums`,children:t.width?`${t.width}${a.px}`:`—`}),d(A,{className:`text-center tabular-nums`,children:t.height?`${t.height}${a.px}`:`—`}),d(A,{className:`text-center text-sm text-muted-foreground`,children:t.fit??`cover`}),d(A,{className:`text-center text-sm text-muted-foreground`,children:t.format??`webp`}),d(A,{className:`text-center tabular-nums`,children:t.quality??80}),d(A,{children:f(`div`,{className:P(`flex items-center gap-1 justify-end`,i?.actions),children:[d(v,{variant:`ghost`,size:`sm`,type:`button`,onClick:()=>ie(e),title:a.editStyle,children:d(o,{className:`h-3.5 w-3.5`})}),d(v,{variant:`ghost`,size:`sm`,type:`button`,onClick:()=>oe(e),title:a.delete,className:`text-destructive hover:text-destructive`,children:d(l,{className:`h-3.5 w-3.5`})})]})})]},e))})]})}),d(y,{open:u,onOpenChange:m,children:f(b,{className:i?.dialog,children:[f(C,{children:[d(w,{children:h?a.editStyle:a.addStyle}),d(x,{children:h?`Editing "${h}" image style.`:`Create a new image processing preset.`})]}),f(`div`,{className:`space-y-4 py-2`,children:[X&&d(`div`,{className:`text-sm text-destructive`,children:X}),f(`div`,{className:`space-y-2`,children:[d(E,{htmlFor:`style-name`,children:a.styleName}),d(T,{id:`style-name`,value:R,onChange:e=>z(e.target.value),placeholder:`e.g. thumbnail, medium, large`,disabled:!!h})]}),f(`div`,{className:`grid grid-cols-2 gap-4`,children:[f(`div`,{className:`space-y-2`,children:[f(E,{htmlFor:`style-width`,children:[a.width,` (`,a.px,`)`]}),d(T,{id:`style-width`,type:`number`,value:B,onChange:e=>V(e.target.value),placeholder:`e.g. 200`,min:1})]}),f(`div`,{className:`space-y-2`,children:[f(E,{htmlFor:`style-height`,children:[a.height,` (`,a.px,`)`]}),d(T,{id:`style-height`,type:`number`,value:H,onChange:e=>U(e.target.value),placeholder:`e.g. 200`,min:1})]})]}),f(`div`,{className:`grid grid-cols-2 gap-4`,children:[f(`div`,{className:`space-y-2`,children:[d(E,{htmlFor:`style-fit`,children:a.fit}),d(D,{id:`style-fit`,value:W,onChange:e=>G(e.target.value),children:I.map(e=>d(`option`,{value:e,children:e},e))})]}),f(`div`,{className:`space-y-2`,children:[d(E,{htmlFor:`style-format`,children:a.format}),d(D,{id:`style-format`,value:K,onChange:e=>q(e.target.value),children:L.map(e=>d(`option`,{value:e,children:e},e))})]})]}),f(`div`,{className:`space-y-2`,children:[f(E,{htmlFor:`style-quality`,children:[a.quality,` (1-100)`]}),d(T,{id:`style-quality`,type:`number`,value:J,onChange:e=>Y(e.target.value),min:1,max:100})]})]}),f(S,{children:[d(v,{variant:`outline`,type:`button`,onClick:()=>m(!1),children:a.cancel}),d(v,{type:`button`,onClick:ae,children:a.save})]})]})}),d(y,{open:te,onOpenChange:Q,children:f(b,{children:[f(C,{children:[d(w,{children:a.deleteConfirmTitle}),f(x,{children:[a.deleteConfirmDescription,$&&d(`span`,{className:`block mt-2 font-mono text-foreground`,children:$})]})]}),f(S,{children:[d(v,{variant:`outline`,type:`button`,onClick:()=>Q(!1),children:a.cancel}),d(v,{variant:`destructive`,type:`button`,onClick:se,children:a.delete})]})]})})]})}const z={description:[`Media.imageVocabulary.description`,`Every photograph is generated in each shape below, at each width. A slot whose ratio matches a shape draws that shape’s files; a slot with any other ratio is cropped in the browser instead.`],usingDefaults:[`Media.imageVocabulary.usingDefaults`,`Nothing is saved here yet. Shown are the toolkit defaults; until you save, the site uses them — or the shapes configured in its code, if it has any.`],widthsLabel:[`Media.imageVocabulary.widthsLabel`,`Widths (px)`],widthsHelp:[`Media.imageVocabulary.widthsHelp`,`Ascending, separated by commas. A photograph is never enlarged — a width it cannot supply is skipped.`],widthsInvalid:[`Media.imageVocabulary.widthsInvalid`,`Use up to {count} ascending whole numbers between {min} and {max}.`],shapesLabel:[`Media.imageVocabulary.shapesLabel`,`Shapes`],naturalNote:[`Media.imageVocabulary.naturalNote`,`Plus “natural” — each photograph at its own shape, over the same widths.`],columnName:[`Media.imageVocabulary.columnName`,`Name`],columnRatio:[`Media.imageVocabulary.columnRatio`,`Ratio (W:H)`],columnWidths:[`Media.imageVocabulary.columnWidths`,`Own widths`],ownWidthsPlaceholder:[`Media.imageVocabulary.ownWidthsPlaceholder`,`All widths`],ratioInvalid:[`Media.imageVocabulary.ratioInvalid`,`Write the ratio as W:H, for example 3:2.`],nameInvalid:[`Media.imageVocabulary.nameInvalid`,`Lowercase letters, digits, - and _, starting with a letter — and not “natural”.`],nameTaken:[`Media.imageVocabulary.nameTaken`,`A shape named “{name}” already exists.`],tooMany:[`Media.imageVocabulary.tooMany`,`At most {count} shapes.`],tooManyFiles:[`Media.imageVocabulary.tooManyFiles`,`That makes {count} files for every photograph; the most is {max}. Remove a shape or a width first.`],add:[`Media.imageVocabulary.add`,`Add shape`],remove:[`Media.imageVocabulary.remove`,`Remove {name}`]},B=Object.values(z).map(([e])=>e);function V(e,t){return t?e.replace(/\{(\w+)\}/g,(e,n)=>n in t?String(t[n]):e):e}function H(){let e=F();return(t,n)=>{let[r,i]=z[t];return e.has(r)?e(r,n):V(i,n)}}function U(e){let t=e.split(`,`).map(e=>e.trim()).filter(e=>e!==``);if(t.length===0||t.length>12)return null;let n=[];for(let e of t){if(!/^\d+$/.test(e))return null;let t=Number(e);if(t<16||t>8192)return null;let r=n[n.length-1];if(r!==void 0&&t<=r)return null;n.push(t)}return n}function W(e,t){return e.length===t.length&&e.every((e,n)=>e===t[n])}function G(e){return e?e.join(`, `):``}function K(e){return t=>{t.key!==`Enter`||t.nativeEvent.isComposing||t.keyCode===229||(t.preventDefault(),e())}}function q(){return{shapes:Object.fromEntries(Object.entries(i.shapes).map(([e,t])=>[e,{...t}])),ladder:[...i.ladder]}}function J(e){let t=JSON.stringify(e.value??null),[n,r]=g(null),[i,a]=g(t),[o,s]=g(0);t!==i&&(a(t),t!==n&&s(o+1));let c=t=>{r(JSON.stringify(t)),e.onChange(t)};return d(Y,{...e,onChange:c},o)}function Y({value:i,onChange:a,error:o,className:c}){let l=H(),u=m(),p=i??q(),_=Object.keys(p.shapes).sort(),[y,b]=g(G(p.ladder)),[x,S]=g(!1),[C,w]=g(``),[D,O]=g(``),[k,A]=g(null),[j,M]=g(null),N=h(null),F=e=>{let t=n(e);return t>64?(M(l(`tooManyFiles`,{count:t,max:64})),!1):(M(null),a(e),!0)},I=()=>{let e=U(y);S(e===null),e&&!W(e,p.ladder)&&F({...p,ladder:e})},L=()=>{let t=C.trim(),n=D.trim();if(!r.test(t)||t===`natural`){A(l(`nameInvalid`));return}if(Object.hasOwn(p.shapes,t)){A(l(`nameTaken`,{name:t}));return}if(_.length>=16){A(l(`tooMany`,{count:16}));return}if(e(n)===null){A(l(`ratioInvalid`));return}A(null),F({...p,shapes:{...p.shapes,[t]:{ratio:n}}})&&(w(``),O(``))},ee=(e,t)=>F({...p,shapes:{...p.shapes,[e]:t}}),R=e=>{let{[e]:t,...n}=p.shapes;F({...p,shapes:n})};return f(`div`,{className:P(`space-y-6`,c),children:[f(`div`,{className:`space-y-1 text-sm text-muted-foreground`,children:[d(`p`,{children:l(`description`)}),i==null?d(`p`,{children:l(`usingDefaults`)}):null]}),j?d(`p`,{role:`alert`,className:`text-sm text-destructive`,children:j}):null,o?d(`div`,{className:`rounded-md border border-destructive/50 bg-destructive/10 px-4 py-3 text-sm text-destructive`,children:o}):null,f(`div`,{className:`space-y-2`,children:[d(E,{htmlFor:`${u}-ladder`,children:l(`widthsLabel`)}),d(T,{id:`${u}-ladder`,value:y,onChange:e=>b(e.target.value),onBlur:I,onKeyDown:K(I),"aria-invalid":x,"aria-describedby":`${u}-ladder-help`,inputMode:`numeric`}),d(`p`,{id:`${u}-ladder-help`,className:P(`text-xs`,x?`text-destructive`:`text-muted-foreground`),children:x?l(`widthsInvalid`,{count:12,min:16,max:t}):l(`widthsHelp`)})]}),f(`fieldset`,{className:`space-y-2`,children:[d(`legend`,{className:`text-sm font-medium`,children:l(`shapesLabel`)}),f(`div`,{className:`grid grid-cols-[minmax(6rem,1fr)_8rem_minmax(8rem,2fr)_2.5rem] gap-2 text-xs text-muted-foreground`,children:[d(`span`,{children:l(`columnName`)}),d(`span`,{children:l(`columnRatio`)}),d(`span`,{children:l(`columnWidths`)}),d(`span`,{})]}),_.map(e=>{let t=p.shapes[e];return t?d(X,{name:e,shape:t,copy:l,onReplace:t=>ee(e,t),onRemove:()=>R(e)},e):null}),d(`p`,{className:`text-xs text-muted-foreground`,children:l(`naturalNote`)}),f(`div`,{className:`grid grid-cols-[minmax(6rem,1fr)_8rem_auto] items-start gap-2 pt-2`,children:[d(T,{"aria-label":l(`columnName`),value:C,onChange:e=>w(e.target.value),onKeyDown:K(()=>N.current?.focus())}),d(T,{ref:N,"aria-label":l(`columnRatio`),value:D,onChange:e=>O(e.target.value),onKeyDown:K(L),placeholder:`3:2`}),f(v,{type:`button`,size:`sm`,onClick:L,children:[d(s,{className:`mr-1 h-4 w-4`}),l(`add`)]})]}),k?d(`p`,{role:`alert`,className:`text-xs text-destructive`,children:k}):null]})]})}function X({name:n,shape:r,copy:i,onReplace:a,onRemove:o}){let[s,c]=g(r.ratio),[u,p]=g(G(r.widths)),[m,h]=g(null),_=()=>{let t=e(s.trim())!==null;h(t?null:`ratioInvalid`),t&&s.trim()!==r.ratio&&a({...r,ratio:s.trim()})},y=()=>{if(u.trim()===``){h(null);let{widths:e,...t}=r;r.widths&&a(t);return}let e=U(u);h(e?null:`widthsInvalid`),e&&!W(e,r.widths??[])&&a({...r,widths:e})};return f(`div`,{className:`space-y-1`,children:[f(`div`,{className:`grid grid-cols-[minmax(6rem,1fr)_8rem_minmax(8rem,2fr)_2.5rem] items-center gap-2`,children:[d(`span`,{className:`font-mono text-sm`,children:n}),d(T,{"aria-label":`${n} ${i(`columnRatio`)}`,value:s,onChange:e=>c(e.target.value),onBlur:_,onKeyDown:K(_),"aria-invalid":m===`ratioInvalid`}),d(T,{"aria-label":`${n} ${i(`columnWidths`)}`,value:u,onChange:e=>p(e.target.value),onBlur:y,onKeyDown:K(y),placeholder:i(`ownWidthsPlaceholder`),"aria-invalid":m===`widthsInvalid`}),d(v,{type:`button`,variant:`ghost`,size:`sm`,onClick:o,title:i(`remove`,{name:n}),"aria-label":i(`remove`,{name:n}),className:`text-destructive hover:text-destructive`,children:d(l,{className:`h-3.5 w-3.5`})})]}),m?d(`p`,{role:`alert`,className:`text-xs text-destructive`,children:i(m,{count:12,min:16,max:t})}):null]})}const Z={workerRequired:[`Media.variantGeneration.workerRequired`,`Image sizes are generated in the background by the queue worker — a separate process that has to run beside the site (pnpm worker). Without it, the site still works, but new and changed images are shown at full size.`],regenerate:[`Media.variantGeneration.regenerate`,`Catch up all images`],regenerating:[`Media.variantGeneration.regenerating`,`Starting…`],queued:[`Media.variantGeneration.queued`,`Started. The queue worker catches the library up in the background; the site keeps serving meanwhile. Follow it on the Queue page.`],alreadyRunning:[`Media.variantGeneration.alreadyRunning`,`A catch-up is already running — follow it on the Queue page.`],inlineDone:[`Media.variantGeneration.inlineDone`,`Processed {processed} of {total}; skipped {skipped}; errors {errors}.`],failed:[`Media.variantGeneration.failed`,`Could not start the catch-up: {reason}`]};Object.values(Z).map(([e])=>e);function te(e,t){return t?e.replace(/\{(\w+)\}/g,(e,n)=>n in t?String(t[n]):e):e}function Q({apiBasePath:e,labels:t}){let n=F(),r=(e,t)=>{let[r,i]=Z[e];return n.has(r)?n(r,t):te(i,t)},[i,o]=g(!1),[s,l]=g(null),m=p(async()=>{o(!0),l(null);try{let t=await fetch(`${e}/media/regenerate-variants`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({})});if(!t.ok){l({kind:`error`,reason:(await t.json().catch(()=>({}))).error??String(t.status)});return}let n=await t.json();l(n.mode===`queued`?{kind:`queued`,alreadyRunning:n.alreadyRunning}:{kind:`inline`,...n})}catch(e){l({kind:`error`,reason:e instanceof Error?e.message:String(e)})}finally{o(!1)}},[e]);return f(`div`,{className:`flex max-w-xl flex-col items-end gap-2`,children:[d(`p`,{className:`text-right text-xs text-muted-foreground`,"data-variant-worker-required":``,children:r(`workerRequired`)}),f(`div`,{className:`flex items-center gap-3`,children:[s?.kind===`queued`&&d(`span`,{className:`text-xs text-muted-foreground`,role:`status`,children:r(s.alreadyRunning?`alreadyRunning`:`queued`)}),s?.kind===`inline`&&d(`span`,{className:`text-xs text-muted-foreground`,role:`status`,children:r(`inlineDone`,{processed:s.processed,total:s.total,skipped:s.skipped,errors:s.errors})}),s?.kind===`error`&&d(`span`,{className:`text-xs text-destructive`,role:`alert`,children:r(`failed`,{reason:s.reason})}),d(v,{variant:`outline`,size:`sm`,type:`button`,onClick:m,disabled:i,children:i?f(u,{children:[d(a,{className:`h-4 w-4 mr-1 animate-spin`}),t?.regenerating??r(`regenerating`)]}):f(u,{children:[d(c,{className:`h-4 w-4 mr-1`}),t?.regenerate??r(`regenerate`)]})})]})]})}export{B as IMAGE_VOCABULARY_MESSAGE_KEYS,R as ImageStylesManager,J as ImageVocabularyEditor,Q as RegenerateVariantsAction};
2
2
  //# sourceMappingURL=image-styles.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"image-styles.mjs","names":["COPY","interpolate"],"sources":["../src/image-styles/image-styles-manager.tsx","../src/image-styles/image-vocabulary-editor.tsx","../src/image-styles/regenerate-variants-action.tsx"],"sourcesContent":["'use client'\n\nimport type { ImageStyle } from '../types.js'\nimport {\n Badge,\n Button,\n cn,\n Dialog,\n DialogContent,\n DialogDescription,\n DialogFooter,\n DialogHeader,\n DialogTitle,\n Input,\n Label,\n Select,\n Table,\n TableBody,\n TableCell,\n TableHead,\n TableHeader,\n TableRow,\n} from '@murumets-ee/ui'\nimport { Pencil, Plus, Trash2 } from 'lucide-react'\nimport { useCallback, useState } from 'react'\nimport type { ImageStylesManagerProps } from './types.js'\n\nconst FIT_OPTIONS = ['cover', 'contain', 'inside', 'outside', 'fill'] as const\nconst FORMAT_OPTIONS = ['webp', 'jpeg', 'png', 'avif'] as const\n\nconst DEFAULT_LABELS = {\n title: 'Image Styles',\n description:\n 'Configure image processing presets. Every image gets a file for each style, generated in the background by the queue worker. Save the form to apply changes.',\n addStyle: 'Add Style',\n editStyle: 'Edit Style',\n styleName: 'Style Name',\n width: 'Width',\n height: 'Height',\n fit: 'Fit',\n format: 'Format',\n quality: 'Quality',\n save: 'Save',\n cancel: 'Cancel',\n delete: 'Delete',\n deleteConfirmTitle: 'Delete Image Style',\n deleteConfirmDescription: 'Are you sure? This will not remove existing variant files.',\n systemBadge: 'system',\n noStyles: 'No image styles configured.',\n px: 'px',\n}\n\n/**\n * Controlled image-styles editor. Designed to slot into the settings-form\n * renderer pattern (`renderer: 'media.imageStyles'`). Holds dialog state\n * internally; commits through `onChange` so the parent settings form's\n * Save button persists the change.\n */\nexport function ImageStylesManager({\n value,\n onChange,\n error,\n labels: userLabels,\n classNames,\n}: ImageStylesManagerProps) {\n const labels = { ...DEFAULT_LABELS, ...userLabels }\n const styles = value ?? {}\n\n // Add / edit dialog\n const [isDialogOpen, setIsDialogOpen] = useState(false)\n const [editingKey, setEditingKey] = useState<string | null>(null)\n const [formName, setFormName] = useState('')\n const [formWidth, setFormWidth] = useState('')\n const [formHeight, setFormHeight] = useState('')\n const [formFit, setFormFit] = useState<string>('cover')\n const [formFormat, setFormFormat] = useState<string>('webp')\n const [formQuality, setFormQuality] = useState('80')\n const [formError, setFormError] = useState<string | null>(null)\n\n // Delete dialog\n const [isDeleteOpen, setIsDeleteOpen] = useState(false)\n const [deleteKey, setDeleteKey] = useState<string | null>(null)\n\n const openAddDialog = useCallback(() => {\n setEditingKey(null)\n setFormName('')\n setFormWidth('')\n setFormHeight('')\n setFormFit('cover')\n setFormFormat('webp')\n setFormQuality('80')\n setFormError(null)\n setIsDialogOpen(true)\n }, [])\n\n const openEditDialog = useCallback(\n (key: string) => {\n const style = styles[key]\n if (!style) return\n setEditingKey(key)\n setFormName(key)\n setFormWidth(style.width?.toString() ?? '')\n setFormHeight(style.height?.toString() ?? '')\n setFormFit(style.fit ?? 'cover')\n setFormFormat(style.format ?? 'webp')\n setFormQuality((style.quality ?? 80).toString())\n setFormError(null)\n setIsDialogOpen(true)\n },\n [styles],\n )\n\n const handleSaveStyle = useCallback(() => {\n setFormError(null)\n\n const name = formName.trim().toLowerCase()\n if (!name || !/^[a-z][a-z0-9_-]*$/.test(name)) {\n setFormError('Name must be lowercase alphanumeric (a-z, 0-9, -, _)')\n return\n }\n if (name !== editingKey && styles[name]) {\n setFormError(`A style named \"${name}\" already exists`)\n return\n }\n\n const w = formWidth ? Number.parseInt(formWidth, 10) : undefined\n const h = formHeight ? Number.parseInt(formHeight, 10) : undefined\n if (!w && !h) {\n setFormError('At least width or height is required')\n return\n }\n if (w !== undefined && (Number.isNaN(w) || w <= 0)) {\n setFormError('Width must be a positive number')\n return\n }\n if (h !== undefined && (Number.isNaN(h) || h <= 0)) {\n setFormError('Height must be a positive number')\n return\n }\n const q = Number.parseInt(formQuality, 10)\n if (Number.isNaN(q) || q < 1 || q > 100) {\n setFormError('Quality must be 1-100')\n return\n }\n\n const fit = formFit as NonNullable<ImageStyle['fit']>\n const format = formFormat as NonNullable<ImageStyle['format']>\n const style: ImageStyle = {\n ...(w ? { width: w } : {}),\n ...(h ? { height: h } : {}),\n fit,\n format,\n quality: q,\n }\n\n const next = { ...styles }\n if (editingKey && editingKey !== name) {\n delete next[editingKey]\n }\n next[name] = style\n onChange(next)\n setIsDialogOpen(false)\n }, [\n styles,\n editingKey,\n formName,\n formWidth,\n formHeight,\n formFit,\n formFormat,\n formQuality,\n onChange,\n ])\n\n const openDeleteDialog = useCallback((key: string) => {\n setDeleteKey(key)\n setIsDeleteOpen(true)\n }, [])\n\n const handleDelete = useCallback(() => {\n if (!deleteKey) return\n const next = { ...styles }\n delete next[deleteKey]\n onChange(next)\n setIsDeleteOpen(false)\n setDeleteKey(null)\n }, [deleteKey, styles, onChange])\n\n const sortedEntries = Object.entries(styles).sort(([a], [b]) => a.localeCompare(b))\n\n return (\n <div className={cn('space-y-4', classNames?.root)}>\n <div className={cn('flex items-start justify-between', classNames?.header)}>\n <p className=\"text-sm text-muted-foreground\">{labels.description}</p>\n <Button onClick={openAddDialog} size=\"sm\" type=\"button\">\n <Plus className=\"h-4 w-4 mr-1\" />\n {labels.addStyle}\n </Button>\n </div>\n\n {error && (\n <div className=\"rounded-md border border-destructive/50 bg-destructive/10 px-4 py-3 text-sm text-destructive\">\n {error}\n </div>\n )}\n\n {sortedEntries.length === 0 ? (\n <p className=\"text-sm text-muted-foreground py-8 text-center\">{labels.noStyles}</p>\n ) : (\n <div className={cn('rounded-md border', classNames?.table)}>\n <Table>\n <TableHeader>\n <TableRow>\n <TableHead>{labels.styleName}</TableHead>\n <TableHead className=\"text-center\">{labels.width}</TableHead>\n <TableHead className=\"text-center\">{labels.height}</TableHead>\n <TableHead className=\"text-center\">{labels.fit}</TableHead>\n <TableHead className=\"text-center\">{labels.format}</TableHead>\n <TableHead className=\"text-center\">{labels.quality}</TableHead>\n <TableHead className=\"w-24\" />\n </TableRow>\n </TableHeader>\n <TableBody>\n {sortedEntries.map(([name, style]) => (\n <TableRow key={name}>\n <TableCell className=\"font-mono text-sm\">\n {name}\n {name === 'thumbnail' && (\n <Badge variant=\"secondary\" className=\"ml-2 text-xs\">\n {labels.systemBadge}\n </Badge>\n )}\n </TableCell>\n <TableCell className=\"text-center tabular-nums\">\n {style.width ? `${style.width}${labels.px}` : '—'}\n </TableCell>\n <TableCell className=\"text-center tabular-nums\">\n {style.height ? `${style.height}${labels.px}` : '—'}\n </TableCell>\n <TableCell className=\"text-center text-sm text-muted-foreground\">\n {style.fit ?? 'cover'}\n </TableCell>\n <TableCell className=\"text-center text-sm text-muted-foreground\">\n {style.format ?? 'webp'}\n </TableCell>\n <TableCell className=\"text-center tabular-nums\">{style.quality ?? 80}</TableCell>\n <TableCell>\n <div className={cn('flex items-center gap-1 justify-end', classNames?.actions)}>\n <Button\n variant=\"ghost\"\n size=\"sm\"\n type=\"button\"\n onClick={() => openEditDialog(name)}\n title={labels.editStyle}\n >\n <Pencil className=\"h-3.5 w-3.5\" />\n </Button>\n <Button\n variant=\"ghost\"\n size=\"sm\"\n type=\"button\"\n onClick={() => openDeleteDialog(name)}\n title={labels.delete}\n className=\"text-destructive hover:text-destructive\"\n >\n <Trash2 className=\"h-3.5 w-3.5\" />\n </Button>\n </div>\n </TableCell>\n </TableRow>\n ))}\n </TableBody>\n </Table>\n </div>\n )}\n\n <Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>\n <DialogContent className={classNames?.dialog}>\n <DialogHeader>\n <DialogTitle>{editingKey ? labels.editStyle : labels.addStyle}</DialogTitle>\n <DialogDescription>\n {editingKey\n ? `Editing \"${editingKey}\" image style.`\n : 'Create a new image processing preset.'}\n </DialogDescription>\n </DialogHeader>\n\n <div className=\"space-y-4 py-2\">\n {formError && <div className=\"text-sm text-destructive\">{formError}</div>}\n\n <div className=\"space-y-2\">\n <Label htmlFor=\"style-name\">{labels.styleName}</Label>\n <Input\n id=\"style-name\"\n value={formName}\n onChange={(e) => setFormName(e.target.value)}\n placeholder=\"e.g. thumbnail, medium, large\"\n disabled={!!editingKey}\n />\n </div>\n\n <div className=\"grid grid-cols-2 gap-4\">\n <div className=\"space-y-2\">\n <Label htmlFor=\"style-width\">\n {labels.width} ({labels.px})\n </Label>\n <Input\n id=\"style-width\"\n type=\"number\"\n value={formWidth}\n onChange={(e) => setFormWidth(e.target.value)}\n placeholder=\"e.g. 200\"\n min={1}\n />\n </div>\n <div className=\"space-y-2\">\n <Label htmlFor=\"style-height\">\n {labels.height} ({labels.px})\n </Label>\n <Input\n id=\"style-height\"\n type=\"number\"\n value={formHeight}\n onChange={(e) => setFormHeight(e.target.value)}\n placeholder=\"e.g. 200\"\n min={1}\n />\n </div>\n </div>\n\n <div className=\"grid grid-cols-2 gap-4\">\n <div className=\"space-y-2\">\n <Label htmlFor=\"style-fit\">{labels.fit}</Label>\n <Select id=\"style-fit\" value={formFit} onChange={(e) => setFormFit(e.target.value)}>\n {FIT_OPTIONS.map((f) => (\n <option key={f} value={f}>\n {f}\n </option>\n ))}\n </Select>\n </div>\n <div className=\"space-y-2\">\n <Label htmlFor=\"style-format\">{labels.format}</Label>\n <Select\n id=\"style-format\"\n value={formFormat}\n onChange={(e) => setFormFormat(e.target.value)}\n >\n {FORMAT_OPTIONS.map((f) => (\n <option key={f} value={f}>\n {f}\n </option>\n ))}\n </Select>\n </div>\n </div>\n\n <div className=\"space-y-2\">\n <Label htmlFor=\"style-quality\">{labels.quality} (1-100)</Label>\n <Input\n id=\"style-quality\"\n type=\"number\"\n value={formQuality}\n onChange={(e) => setFormQuality(e.target.value)}\n min={1}\n max={100}\n />\n </div>\n </div>\n\n <DialogFooter>\n <Button variant=\"outline\" type=\"button\" onClick={() => setIsDialogOpen(false)}>\n {labels.cancel}\n </Button>\n <Button type=\"button\" onClick={handleSaveStyle}>\n {labels.save}\n </Button>\n </DialogFooter>\n </DialogContent>\n </Dialog>\n\n <Dialog open={isDeleteOpen} onOpenChange={setIsDeleteOpen}>\n <DialogContent>\n <DialogHeader>\n <DialogTitle>{labels.deleteConfirmTitle}</DialogTitle>\n <DialogDescription>\n {labels.deleteConfirmDescription}\n {deleteKey && (\n <span className=\"block mt-2 font-mono text-foreground\">{deleteKey}</span>\n )}\n </DialogDescription>\n </DialogHeader>\n <DialogFooter>\n <Button variant=\"outline\" type=\"button\" onClick={() => setIsDeleteOpen(false)}>\n {labels.cancel}\n </Button>\n <Button variant=\"destructive\" type=\"button\" onClick={handleDelete}>\n {labels.delete}\n </Button>\n </DialogFooter>\n </DialogContent>\n </Dialog>\n </div>\n )\n}\n","'use client'\n\nimport type { ImageShape, ImageVocabulary } from '../types.js'\nimport { Button, cn, Input, Label } from '@murumets-ee/ui'\nimport { Plus, Trash2 } from 'lucide-react'\nimport { useTranslations } from 'next-intl'\nimport { type KeyboardEvent, useId, useRef, useState } from 'react'\nimport {\n DEFAULT_IMAGE_VOCABULARY,\n MAX_LADDER_RUNGS,\n MAX_SHAPES,\n MAX_VARIANT_WIDTH,\n MIN_VARIANT_WIDTH,\n NATURAL_SHAPE,\n parseShapeRatio,\n plannedFileCount,\n SHAPE_NAME_PATTERN,\n} from '../shapes.js'\nimport { MAX_VARIANTS_PER_IMAGE } from '../variant-plan.js'\nimport type { ImageVocabularyEditorProps } from './types.js'\n\n/**\n * Every visible string, as a next-intl KEY plus the English fallback — the\n * interface-copy rule. The keys live in this package's catalogue\n * (`src/messages/*.json`, `Media.imageVocabulary.*`).\n */\nconst COPY = {\n description: [\n 'Media.imageVocabulary.description',\n 'Every photograph is generated in each shape below, at each width. A slot that names a shape draws these files; a slot with its own ratio is cropped in the browser instead.',\n ],\n usingDefaults: [\n 'Media.imageVocabulary.usingDefaults',\n 'Nothing is saved here yet. Shown are the toolkit defaults; until you save, the site uses them — or the shapes configured in its code, if it has any.',\n ],\n widthsLabel: ['Media.imageVocabulary.widthsLabel', 'Widths (px)'],\n widthsHelp: [\n 'Media.imageVocabulary.widthsHelp',\n 'Ascending, separated by commas. A photograph is never enlarged — a width it cannot supply is skipped.',\n ],\n widthsInvalid: [\n 'Media.imageVocabulary.widthsInvalid',\n 'Use up to {count} ascending whole numbers between {min} and {max}.',\n ],\n shapesLabel: ['Media.imageVocabulary.shapesLabel', 'Shapes'],\n naturalNote: [\n 'Media.imageVocabulary.naturalNote',\n 'Plus “natural” — each photograph at its own shape, over the same widths.',\n ],\n columnName: ['Media.imageVocabulary.columnName', 'Name'],\n columnRatio: ['Media.imageVocabulary.columnRatio', 'Ratio (W:H)'],\n columnWidths: ['Media.imageVocabulary.columnWidths', 'Own widths'],\n ownWidthsPlaceholder: ['Media.imageVocabulary.ownWidthsPlaceholder', 'All widths'],\n ratioInvalid: ['Media.imageVocabulary.ratioInvalid', 'Write the ratio as W:H, for example 3:2.'],\n nameInvalid: [\n 'Media.imageVocabulary.nameInvalid',\n 'Lowercase letters, digits, - and _, starting with a letter — and not “natural”.',\n ],\n nameTaken: ['Media.imageVocabulary.nameTaken', 'A shape named “{name}” already exists.'],\n tooMany: ['Media.imageVocabulary.tooMany', 'At most {count} shapes.'],\n tooManyFiles: [\n 'Media.imageVocabulary.tooManyFiles',\n 'That makes {count} files for every photograph; the most is {max}. Remove a shape or a width first.',\n ],\n add: ['Media.imageVocabulary.add', 'Add shape'],\n remove: ['Media.imageVocabulary.remove', 'Remove {name}'],\n} as const satisfies Record<string, readonly [string, string]>\n\n/** Every catalogue key this component reads — exported for the catalogue test. */\nexport const IMAGE_VOCABULARY_MESSAGE_KEYS: readonly string[] = Object.values(COPY).map(([k]) => k)\n\ntype CopyId = keyof typeof COPY\ntype CopyValues = Record<string, string | number>\n\nfunction interpolate(template: string, values: CopyValues | undefined): string {\n if (!values) return template\n return template.replace(/\\{(\\w+)\\}/g, (whole, name: string) =>\n name in values ? String(values[name]) : whole,\n )\n}\n\n/**\n * Resolve copy with an UNSCOPED translator, falling back to the English LABEL\n * — never to the key — when the app has not merged this package's catalogue.\n */\nfunction useCopy(): (id: CopyId, values?: CopyValues) => string {\n const t = useTranslations()\n return (id, values) => {\n const [key, fallback] = COPY[id]\n return t.has(key) ? t(key, values) : interpolate(fallback, values)\n }\n}\n\n/** `'320, 640,960'` → `[320, 640, 960]`, or `null` unless ascending, distinct, in bounds. */\nexport function parseWidthList(raw: string): number[] | null {\n const parts = raw\n .split(',')\n .map((p) => p.trim())\n .filter((p) => p !== '')\n if (parts.length === 0 || parts.length > MAX_LADDER_RUNGS) return null\n const widths: number[] = []\n for (const part of parts) {\n if (!/^\\d+$/.test(part)) return null\n const w = Number(part)\n if (w < MIN_VARIANT_WIDTH || w > MAX_VARIANT_WIDTH) return null\n const previous = widths[widths.length - 1]\n if (previous !== undefined && w <= previous) return null\n widths.push(w)\n }\n return widths\n}\n\nfunction sameWidths(a: readonly number[], b: readonly number[]): boolean {\n return a.length === b.length && a.every((w, i) => w === b[i])\n}\n\nfunction formatWidths(widths: readonly number[] | undefined): string {\n return widths ? widths.join(', ') : ''\n}\n\n/**\n * Enter commits the field it was pressed in, and never submits the settings\n * form: fields commit on blur, so a form submitted from inside one would save\n * the value from BEFORE the edit and report success.\n */\nfunction onEnter(commit: () => void) {\n return (event: KeyboardEvent<HTMLInputElement>) => {\n // Enter that CONFIRMS an IME candidate is not a commit — Chrome flags it\n // `isComposing`, Safari sends keyCode 229.\n if (event.key !== 'Enter' || event.nativeEvent.isComposing || event.keyCode === 229) return\n event.preventDefault()\n commit()\n }\n}\n\n/** The seeded vocabulary as an editable value — the shared constant is frozen. */\nfunction copyOfDefaults(): ImageVocabulary {\n return {\n shapes: Object.fromEntries(\n Object.entries(DEFAULT_IMAGE_VOCABULARY.shapes).map(([name, shape]) => [name, { ...shape }]),\n ),\n ladder: [...DEFAULT_IMAGE_VOCABULARY.ladder],\n }\n}\n\n/**\n * The editor for `media.imageStyles` → `vocabulary`: the shape vocabulary and\n * the width ladder (images D003). Controlled — every valid edit commits through\n * `onChange`, and the settings form's Save persists it; an invalid one stays in\n * its own field with the reason beside it and commits nothing.\n *\n * `value` is `null` until something is saved, because the key deliberately\n * has no settings default (so the plugin-config tier stays reachable). The\n * editor then shows the seeded vocabulary and says so.\n */\nexport function ImageVocabularyEditor(props: ImageVocabularyEditorProps) {\n // The fields keep DRAFTS (a half-typed ratio must not be thrown away), so an\n // EXTERNAL change to `value` — a form reset, a reload — has to re-seed them.\n // Our own commits must not: remounting on those would drop focus from the\n // field the operator just tabbed into. So the fields remount on a value this\n // editor did not produce, tracked during render rather than in an effect.\n const serialized = JSON.stringify(props.value ?? null)\n const [committed, setCommitted] = useState<string | null>(null)\n const [seen, setSeen] = useState(serialized)\n const [epoch, setEpoch] = useState(0)\n if (serialized !== seen) {\n setSeen(serialized)\n if (serialized !== committed) setEpoch(epoch + 1)\n }\n const onChange = (next: ImageVocabulary) => {\n setCommitted(JSON.stringify(next))\n props.onChange(next)\n }\n return <VocabularyFields key={epoch} {...props} onChange={onChange} />\n}\n\nfunction VocabularyFields({ value, onChange, error, className }: ImageVocabularyEditorProps) {\n const copy = useCopy()\n const ids = useId()\n const vocabulary: ImageVocabulary = value ?? copyOfDefaults()\n const names = Object.keys(vocabulary.shapes).sort()\n\n const [ladderDraft, setLadderDraft] = useState(formatWidths(vocabulary.ladder))\n const [ladderError, setLadderError] = useState(false)\n const [newName, setNewName] = useState('')\n const [newRatio, setNewRatio] = useState('')\n const [addError, setAddError] = useState<string | null>(null)\n const [capError, setCapError] = useState<string | null>(null)\n const ratioInput = useRef<HTMLInputElement>(null)\n\n /**\n * Commit, unless the result plans more files per photograph than the cap —\n * the settings route would refuse it on Save, so refuse it here, where the\n * operator is looking, instead.\n */\n const commit = (next: ImageVocabulary): boolean => {\n const count = plannedFileCount(next)\n if (count > MAX_VARIANTS_PER_IMAGE) {\n setCapError(copy('tooManyFiles', { count, max: MAX_VARIANTS_PER_IMAGE }))\n return false\n }\n setCapError(null)\n onChange(next)\n return true\n }\n\n const commitLadder = () => {\n const parsed = parseWidthList(ladderDraft)\n setLadderError(parsed === null)\n // Blurring an untouched field must commit NOTHING: with no stored value the\n // editor is showing the defaults, and committing them would make the next\n // Save store them — silently shadowing a vocabulary configured in code.\n if (parsed && !sameWidths(parsed, vocabulary.ladder)) commit({ ...vocabulary, ladder: parsed })\n }\n\n const addShape = () => {\n const name = newName.trim()\n const ratio = newRatio.trim()\n if (!SHAPE_NAME_PATTERN.test(name) || name === NATURAL_SHAPE) {\n setAddError(copy('nameInvalid'))\n return\n }\n // `hasOwn`: `constructor` passes the name rule, and a plain lookup finds\n // `Object.prototype.constructor` — \"taken\" for a name nobody used.\n if (Object.hasOwn(vocabulary.shapes, name)) {\n setAddError(copy('nameTaken', { name }))\n return\n }\n if (names.length >= MAX_SHAPES) {\n setAddError(copy('tooMany', { count: MAX_SHAPES }))\n return\n }\n if (parseShapeRatio(ratio) === null) {\n setAddError(copy('ratioInvalid'))\n return\n }\n setAddError(null)\n if (commit({ ...vocabulary, shapes: { ...vocabulary.shapes, [name]: { ratio } } })) {\n setNewName('')\n setNewRatio('')\n }\n }\n\n const replaceShape = (name: string, shape: ImageShape) =>\n commit({ ...vocabulary, shapes: { ...vocabulary.shapes, [name]: shape } })\n\n const removeShape = (name: string) => {\n const { [name]: _removed, ...rest } = vocabulary.shapes\n commit({ ...vocabulary, shapes: rest })\n }\n\n return (\n <div className={cn('space-y-6', className)}>\n <div className=\"space-y-1 text-sm text-muted-foreground\">\n <p>{copy('description')}</p>\n {value === null || value === undefined ? <p>{copy('usingDefaults')}</p> : null}\n </div>\n\n {capError ? (\n <p role=\"alert\" className=\"text-sm text-destructive\">\n {capError}\n </p>\n ) : null}\n\n {error ? (\n <div className=\"rounded-md border border-destructive/50 bg-destructive/10 px-4 py-3 text-sm text-destructive\">\n {error}\n </div>\n ) : null}\n\n <div className=\"space-y-2\">\n <Label htmlFor={`${ids}-ladder`}>{copy('widthsLabel')}</Label>\n <Input\n id={`${ids}-ladder`}\n value={ladderDraft}\n onChange={(e) => setLadderDraft(e.target.value)}\n onBlur={commitLadder}\n onKeyDown={onEnter(commitLadder)}\n aria-invalid={ladderError}\n aria-describedby={`${ids}-ladder-help`}\n inputMode=\"numeric\"\n />\n <p\n id={`${ids}-ladder-help`}\n className={cn('text-xs', ladderError ? 'text-destructive' : 'text-muted-foreground')}\n >\n {ladderError\n ? copy('widthsInvalid', {\n count: MAX_LADDER_RUNGS,\n min: MIN_VARIANT_WIDTH,\n max: MAX_VARIANT_WIDTH,\n })\n : copy('widthsHelp')}\n </p>\n </div>\n\n <fieldset className=\"space-y-2\">\n <legend className=\"text-sm font-medium\">{copy('shapesLabel')}</legend>\n <div className=\"grid grid-cols-[minmax(6rem,1fr)_8rem_minmax(8rem,2fr)_2.5rem] gap-2 text-xs text-muted-foreground\">\n <span>{copy('columnName')}</span>\n <span>{copy('columnRatio')}</span>\n <span>{copy('columnWidths')}</span>\n <span />\n </div>\n {names.map((name) => {\n const shape = vocabulary.shapes[name]\n return shape ? (\n <ShapeRow\n key={name}\n name={name}\n shape={shape}\n copy={copy}\n onReplace={(next) => replaceShape(name, next)}\n onRemove={() => removeShape(name)}\n />\n ) : null\n })}\n <p className=\"text-xs text-muted-foreground\">{copy('naturalNote')}</p>\n\n <div className=\"grid grid-cols-[minmax(6rem,1fr)_8rem_auto] items-start gap-2 pt-2\">\n <Input\n aria-label={copy('columnName')}\n value={newName}\n onChange={(e) => setNewName(e.target.value)}\n onKeyDown={onEnter(() => ratioInput.current?.focus())}\n />\n <Input\n ref={ratioInput}\n aria-label={copy('columnRatio')}\n value={newRatio}\n onChange={(e) => setNewRatio(e.target.value)}\n onKeyDown={onEnter(addShape)}\n placeholder=\"3:2\"\n />\n <Button type=\"button\" size=\"sm\" onClick={addShape}>\n <Plus className=\"mr-1 h-4 w-4\" />\n {copy('add')}\n </Button>\n </div>\n {addError ? (\n <p role=\"alert\" className=\"text-xs text-destructive\">\n {addError}\n </p>\n ) : null}\n </fieldset>\n </div>\n )\n}\n\nfunction ShapeRow({\n name,\n shape,\n copy,\n onReplace,\n onRemove,\n}: {\n name: string\n shape: ImageShape\n copy: ReturnType<typeof useCopy>\n onReplace: (next: ImageShape) => boolean\n onRemove: () => void\n}) {\n const [ratio, setRatio] = useState(shape.ratio)\n const [widths, setWidths] = useState(formatWidths(shape.widths))\n const [problem, setProblem] = useState<CopyId | null>(null)\n\n const commitRatio = () => {\n const ok = parseShapeRatio(ratio.trim()) !== null\n setProblem(ok ? null : 'ratioInvalid')\n if (ok && ratio.trim() !== shape.ratio) onReplace({ ...shape, ratio: ratio.trim() })\n }\n\n const commitWidths = () => {\n if (widths.trim() === '') {\n setProblem(null)\n const { widths: _dropped, ...rest } = shape\n if (shape.widths) onReplace(rest)\n return\n }\n const parsed = parseWidthList(widths)\n setProblem(parsed ? null : 'widthsInvalid')\n if (parsed && !sameWidths(parsed, shape.widths ?? [])) onReplace({ ...shape, widths: parsed })\n }\n\n return (\n <div className=\"space-y-1\">\n <div className=\"grid grid-cols-[minmax(6rem,1fr)_8rem_minmax(8rem,2fr)_2.5rem] items-center gap-2\">\n <span className=\"font-mono text-sm\">{name}</span>\n <Input\n aria-label={`${name} ${copy('columnRatio')}`}\n value={ratio}\n onChange={(e) => setRatio(e.target.value)}\n onBlur={commitRatio}\n onKeyDown={onEnter(commitRatio)}\n aria-invalid={problem === 'ratioInvalid'}\n />\n <Input\n aria-label={`${name} ${copy('columnWidths')}`}\n value={widths}\n onChange={(e) => setWidths(e.target.value)}\n onBlur={commitWidths}\n onKeyDown={onEnter(commitWidths)}\n placeholder={copy('ownWidthsPlaceholder')}\n aria-invalid={problem === 'widthsInvalid'}\n />\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n onClick={onRemove}\n title={copy('remove', { name })}\n aria-label={copy('remove', { name })}\n className=\"text-destructive hover:text-destructive\"\n >\n <Trash2 className=\"h-3.5 w-3.5\" />\n </Button>\n </div>\n {problem ? (\n <p role=\"alert\" className=\"text-xs text-destructive\">\n {copy(problem, {\n count: MAX_LADDER_RUNGS,\n min: MIN_VARIANT_WIDTH,\n max: MAX_VARIANT_WIDTH,\n })}\n </p>\n ) : null}\n </div>\n )\n}\n","'use client'\n\nimport { Button } from '@murumets-ee/ui'\nimport { Loader2, RefreshCw } from 'lucide-react'\nimport { useTranslations } from 'next-intl'\nimport { useCallback, useState } from 'react'\nimport type { RegenerateVariantsActionProps } from './types.js'\n\n/**\n * Every visible string, as a next-intl KEY plus the English fallback — the\n * interface-copy rule. Keys live in `src/messages/*.json` (`Media.variantGeneration.*`).\n */\nconst COPY = {\n workerRequired: [\n 'Media.variantGeneration.workerRequired',\n 'Image sizes are generated in the background by the queue worker — a separate process that has to run beside the site (pnpm worker). Without it, the site still works, but new and changed images are shown at full size.',\n ],\n regenerate: ['Media.variantGeneration.regenerate', 'Catch up all images'],\n regenerating: ['Media.variantGeneration.regenerating', 'Starting…'],\n queued: [\n 'Media.variantGeneration.queued',\n 'Started. The queue worker catches the library up in the background; the site keeps serving meanwhile. Follow it on the Queue page.',\n ],\n alreadyRunning: [\n 'Media.variantGeneration.alreadyRunning',\n 'A catch-up is already running — follow it on the Queue page.',\n ],\n inlineDone: [\n 'Media.variantGeneration.inlineDone',\n 'Processed {processed} of {total}; skipped {skipped}; errors {errors}.',\n ],\n failed: ['Media.variantGeneration.failed', 'Could not start the catch-up: {reason}'],\n} as const satisfies Record<string, readonly [string, string]>\n\n/** Every catalogue key this component reads — exported for the catalogue test. */\nexport const REGENERATE_VARIANTS_MESSAGE_KEYS: readonly string[] = Object.values(COPY).map(\n ([k]) => k,\n)\n\ntype CopyId = keyof typeof COPY\ntype CopyValues = Record<string, string | number>\n\nfunction interpolate(template: string, values: CopyValues | undefined): string {\n if (!values) return template\n return template.replace(/\\{(\\w+)\\}/g, (whole, name: string) =>\n name in values ? String(values[name]) : whole,\n )\n}\n\n/** What `POST /media/regenerate-variants` answers. */\ntype RegenerateResponse =\n | { mode: 'queued'; jobId: string; alreadyRunning: boolean }\n | { mode: 'inline'; total: number; processed: number; skipped: number; errors: number }\n\ntype Outcome =\n | { kind: 'queued'; alreadyRunning: boolean }\n | { kind: 'inline'; total: number; processed: number; skipped: number; errors: number }\n | { kind: 'error'; reason: string }\n\n/**\n * The header action on `/admin/settings/media.imageStyles`.\n *\n * Says, always and before anyone clicks, that sizes come from the queue\n * worker — an operator who has never heard of the worker must learn it here,\n * not from a slow page (images C6). The button then asks the server to catch\n * the library up: with a queue that QUEUES a background backfill and returns at\n * once (202); only an app with no queue runs it inside the request, as before.\n */\nexport function RegenerateVariantsAction({\n apiBasePath,\n labels: userLabels,\n}: RegenerateVariantsActionProps) {\n const t = useTranslations()\n const copy = (id: CopyId, values?: CopyValues): string => {\n const [key, fallback] = COPY[id]\n return t.has(key) ? t(key, values) : interpolate(fallback, values)\n }\n const [isRunning, setIsRunning] = useState(false)\n const [outcome, setOutcome] = useState<Outcome | null>(null)\n\n const handleClick = useCallback(async () => {\n setIsRunning(true)\n setOutcome(null)\n try {\n const res = await fetch(`${apiBasePath}/media/regenerate-variants`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({}),\n })\n if (!res.ok) {\n const data = (await res.json().catch(() => ({}))) as { error?: string }\n setOutcome({ kind: 'error', reason: data.error ?? String(res.status) })\n return\n }\n const data = (await res.json()) as RegenerateResponse\n setOutcome(\n data.mode === 'queued'\n ? { kind: 'queued', alreadyRunning: data.alreadyRunning }\n : { kind: 'inline', ...data },\n )\n } catch (err) {\n setOutcome({ kind: 'error', reason: err instanceof Error ? err.message : String(err) })\n } finally {\n setIsRunning(false)\n }\n }, [apiBasePath])\n\n return (\n <div className=\"flex max-w-xl flex-col items-end gap-2\">\n <p className=\"text-right text-xs text-muted-foreground\" data-variant-worker-required=\"\">\n {copy('workerRequired')}\n </p>\n <div className=\"flex items-center gap-3\">\n {outcome?.kind === 'queued' && (\n <span className=\"text-xs text-muted-foreground\" role=\"status\">\n {copy(outcome.alreadyRunning ? 'alreadyRunning' : 'queued')}\n </span>\n )}\n {outcome?.kind === 'inline' && (\n <span className=\"text-xs text-muted-foreground\" role=\"status\">\n {copy('inlineDone', {\n processed: outcome.processed,\n total: outcome.total,\n skipped: outcome.skipped,\n errors: outcome.errors,\n })}\n </span>\n )}\n {outcome?.kind === 'error' && (\n <span className=\"text-xs text-destructive\" role=\"alert\">\n {copy('failed', { reason: outcome.reason })}\n </span>\n )}\n <Button\n variant=\"outline\"\n size=\"sm\"\n type=\"button\"\n onClick={handleClick}\n disabled={isRunning}\n >\n {isRunning ? (\n <>\n <Loader2 className=\"h-4 w-4 mr-1 animate-spin\" />\n {userLabels?.regenerating ?? copy('regenerating')}\n </>\n ) : (\n <>\n <RefreshCw className=\"h-4 w-4 mr-1\" />\n {userLabels?.regenerate ?? copy('regenerate')}\n </>\n )}\n </Button>\n </div>\n </div>\n )\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"image-styles.mjs","names":["COPY","interpolate"],"sources":["../src/image-styles/image-styles-manager.tsx","../src/image-styles/image-vocabulary-editor.tsx","../src/image-styles/regenerate-variants-action.tsx"],"sourcesContent":["'use client'\n\nimport type { ImageStyle } from '../types.js'\nimport {\n Badge,\n Button,\n cn,\n Dialog,\n DialogContent,\n DialogDescription,\n DialogFooter,\n DialogHeader,\n DialogTitle,\n Input,\n Label,\n Select,\n Table,\n TableBody,\n TableCell,\n TableHead,\n TableHeader,\n TableRow,\n} from '@murumets-ee/ui'\nimport { Pencil, Plus, Trash2 } from 'lucide-react'\nimport { useCallback, useState } from 'react'\nimport type { ImageStylesManagerProps } from './types.js'\n\nconst FIT_OPTIONS = ['cover', 'contain', 'inside', 'outside', 'fill'] as const\nconst FORMAT_OPTIONS = ['webp', 'jpeg', 'png', 'avif'] as const\n\nconst DEFAULT_LABELS = {\n title: 'Image Styles',\n description:\n 'Configure image processing presets. Every image gets a file for each style, generated in the background by the queue worker. Save the form to apply changes.',\n addStyle: 'Add Style',\n editStyle: 'Edit Style',\n styleName: 'Style Name',\n width: 'Width',\n height: 'Height',\n fit: 'Fit',\n format: 'Format',\n quality: 'Quality',\n save: 'Save',\n cancel: 'Cancel',\n delete: 'Delete',\n deleteConfirmTitle: 'Delete Image Style',\n deleteConfirmDescription: 'Are you sure? This will not remove existing variant files.',\n systemBadge: 'system',\n noStyles: 'No image styles configured.',\n px: 'px',\n}\n\n/**\n * Controlled image-styles editor. Designed to slot into the settings-form\n * renderer pattern (`renderer: 'media.imageStyles'`). Holds dialog state\n * internally; commits through `onChange` so the parent settings form's\n * Save button persists the change.\n */\nexport function ImageStylesManager({\n value,\n onChange,\n error,\n labels: userLabels,\n classNames,\n}: ImageStylesManagerProps) {\n const labels = { ...DEFAULT_LABELS, ...userLabels }\n const styles = value ?? {}\n\n // Add / edit dialog\n const [isDialogOpen, setIsDialogOpen] = useState(false)\n const [editingKey, setEditingKey] = useState<string | null>(null)\n const [formName, setFormName] = useState('')\n const [formWidth, setFormWidth] = useState('')\n const [formHeight, setFormHeight] = useState('')\n const [formFit, setFormFit] = useState<string>('cover')\n const [formFormat, setFormFormat] = useState<string>('webp')\n const [formQuality, setFormQuality] = useState('80')\n const [formError, setFormError] = useState<string | null>(null)\n\n // Delete dialog\n const [isDeleteOpen, setIsDeleteOpen] = useState(false)\n const [deleteKey, setDeleteKey] = useState<string | null>(null)\n\n const openAddDialog = useCallback(() => {\n setEditingKey(null)\n setFormName('')\n setFormWidth('')\n setFormHeight('')\n setFormFit('cover')\n setFormFormat('webp')\n setFormQuality('80')\n setFormError(null)\n setIsDialogOpen(true)\n }, [])\n\n const openEditDialog = useCallback(\n (key: string) => {\n const style = styles[key]\n if (!style) return\n setEditingKey(key)\n setFormName(key)\n setFormWidth(style.width?.toString() ?? '')\n setFormHeight(style.height?.toString() ?? '')\n setFormFit(style.fit ?? 'cover')\n setFormFormat(style.format ?? 'webp')\n setFormQuality((style.quality ?? 80).toString())\n setFormError(null)\n setIsDialogOpen(true)\n },\n [styles],\n )\n\n const handleSaveStyle = useCallback(() => {\n setFormError(null)\n\n const name = formName.trim().toLowerCase()\n if (!name || !/^[a-z][a-z0-9_-]*$/.test(name)) {\n setFormError('Name must be lowercase alphanumeric (a-z, 0-9, -, _)')\n return\n }\n if (name !== editingKey && styles[name]) {\n setFormError(`A style named \"${name}\" already exists`)\n return\n }\n\n const w = formWidth ? Number.parseInt(formWidth, 10) : undefined\n const h = formHeight ? Number.parseInt(formHeight, 10) : undefined\n if (!w && !h) {\n setFormError('At least width or height is required')\n return\n }\n if (w !== undefined && (Number.isNaN(w) || w <= 0)) {\n setFormError('Width must be a positive number')\n return\n }\n if (h !== undefined && (Number.isNaN(h) || h <= 0)) {\n setFormError('Height must be a positive number')\n return\n }\n const q = Number.parseInt(formQuality, 10)\n if (Number.isNaN(q) || q < 1 || q > 100) {\n setFormError('Quality must be 1-100')\n return\n }\n\n const fit = formFit as NonNullable<ImageStyle['fit']>\n const format = formFormat as NonNullable<ImageStyle['format']>\n const style: ImageStyle = {\n ...(w ? { width: w } : {}),\n ...(h ? { height: h } : {}),\n fit,\n format,\n quality: q,\n }\n\n const next = { ...styles }\n if (editingKey && editingKey !== name) {\n delete next[editingKey]\n }\n next[name] = style\n onChange(next)\n setIsDialogOpen(false)\n }, [\n styles,\n editingKey,\n formName,\n formWidth,\n formHeight,\n formFit,\n formFormat,\n formQuality,\n onChange,\n ])\n\n const openDeleteDialog = useCallback((key: string) => {\n setDeleteKey(key)\n setIsDeleteOpen(true)\n }, [])\n\n const handleDelete = useCallback(() => {\n if (!deleteKey) return\n const next = { ...styles }\n delete next[deleteKey]\n onChange(next)\n setIsDeleteOpen(false)\n setDeleteKey(null)\n }, [deleteKey, styles, onChange])\n\n const sortedEntries = Object.entries(styles).sort(([a], [b]) => a.localeCompare(b))\n\n return (\n <div className={cn('space-y-4', classNames?.root)}>\n <div className={cn('flex items-start justify-between', classNames?.header)}>\n <p className=\"text-sm text-muted-foreground\">{labels.description}</p>\n <Button onClick={openAddDialog} size=\"sm\" type=\"button\">\n <Plus className=\"h-4 w-4 mr-1\" />\n {labels.addStyle}\n </Button>\n </div>\n\n {error && (\n <div className=\"rounded-md border border-destructive/50 bg-destructive/10 px-4 py-3 text-sm text-destructive\">\n {error}\n </div>\n )}\n\n {sortedEntries.length === 0 ? (\n <p className=\"text-sm text-muted-foreground py-8 text-center\">{labels.noStyles}</p>\n ) : (\n <div className={cn('rounded-md border', classNames?.table)}>\n <Table>\n <TableHeader>\n <TableRow>\n <TableHead>{labels.styleName}</TableHead>\n <TableHead className=\"text-center\">{labels.width}</TableHead>\n <TableHead className=\"text-center\">{labels.height}</TableHead>\n <TableHead className=\"text-center\">{labels.fit}</TableHead>\n <TableHead className=\"text-center\">{labels.format}</TableHead>\n <TableHead className=\"text-center\">{labels.quality}</TableHead>\n <TableHead className=\"w-24\" />\n </TableRow>\n </TableHeader>\n <TableBody>\n {sortedEntries.map(([name, style]) => (\n <TableRow key={name}>\n <TableCell className=\"font-mono text-sm\">\n {name}\n {name === 'thumbnail' && (\n <Badge variant=\"secondary\" className=\"ml-2 text-xs\">\n {labels.systemBadge}\n </Badge>\n )}\n </TableCell>\n <TableCell className=\"text-center tabular-nums\">\n {style.width ? `${style.width}${labels.px}` : '—'}\n </TableCell>\n <TableCell className=\"text-center tabular-nums\">\n {style.height ? `${style.height}${labels.px}` : '—'}\n </TableCell>\n <TableCell className=\"text-center text-sm text-muted-foreground\">\n {style.fit ?? 'cover'}\n </TableCell>\n <TableCell className=\"text-center text-sm text-muted-foreground\">\n {style.format ?? 'webp'}\n </TableCell>\n <TableCell className=\"text-center tabular-nums\">{style.quality ?? 80}</TableCell>\n <TableCell>\n <div className={cn('flex items-center gap-1 justify-end', classNames?.actions)}>\n <Button\n variant=\"ghost\"\n size=\"sm\"\n type=\"button\"\n onClick={() => openEditDialog(name)}\n title={labels.editStyle}\n >\n <Pencil className=\"h-3.5 w-3.5\" />\n </Button>\n <Button\n variant=\"ghost\"\n size=\"sm\"\n type=\"button\"\n onClick={() => openDeleteDialog(name)}\n title={labels.delete}\n className=\"text-destructive hover:text-destructive\"\n >\n <Trash2 className=\"h-3.5 w-3.5\" />\n </Button>\n </div>\n </TableCell>\n </TableRow>\n ))}\n </TableBody>\n </Table>\n </div>\n )}\n\n <Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>\n <DialogContent className={classNames?.dialog}>\n <DialogHeader>\n <DialogTitle>{editingKey ? labels.editStyle : labels.addStyle}</DialogTitle>\n <DialogDescription>\n {editingKey\n ? `Editing \"${editingKey}\" image style.`\n : 'Create a new image processing preset.'}\n </DialogDescription>\n </DialogHeader>\n\n <div className=\"space-y-4 py-2\">\n {formError && <div className=\"text-sm text-destructive\">{formError}</div>}\n\n <div className=\"space-y-2\">\n <Label htmlFor=\"style-name\">{labels.styleName}</Label>\n <Input\n id=\"style-name\"\n value={formName}\n onChange={(e) => setFormName(e.target.value)}\n placeholder=\"e.g. thumbnail, medium, large\"\n disabled={!!editingKey}\n />\n </div>\n\n <div className=\"grid grid-cols-2 gap-4\">\n <div className=\"space-y-2\">\n <Label htmlFor=\"style-width\">\n {labels.width} ({labels.px})\n </Label>\n <Input\n id=\"style-width\"\n type=\"number\"\n value={formWidth}\n onChange={(e) => setFormWidth(e.target.value)}\n placeholder=\"e.g. 200\"\n min={1}\n />\n </div>\n <div className=\"space-y-2\">\n <Label htmlFor=\"style-height\">\n {labels.height} ({labels.px})\n </Label>\n <Input\n id=\"style-height\"\n type=\"number\"\n value={formHeight}\n onChange={(e) => setFormHeight(e.target.value)}\n placeholder=\"e.g. 200\"\n min={1}\n />\n </div>\n </div>\n\n <div className=\"grid grid-cols-2 gap-4\">\n <div className=\"space-y-2\">\n <Label htmlFor=\"style-fit\">{labels.fit}</Label>\n <Select id=\"style-fit\" value={formFit} onChange={(e) => setFormFit(e.target.value)}>\n {FIT_OPTIONS.map((f) => (\n <option key={f} value={f}>\n {f}\n </option>\n ))}\n </Select>\n </div>\n <div className=\"space-y-2\">\n <Label htmlFor=\"style-format\">{labels.format}</Label>\n <Select\n id=\"style-format\"\n value={formFormat}\n onChange={(e) => setFormFormat(e.target.value)}\n >\n {FORMAT_OPTIONS.map((f) => (\n <option key={f} value={f}>\n {f}\n </option>\n ))}\n </Select>\n </div>\n </div>\n\n <div className=\"space-y-2\">\n <Label htmlFor=\"style-quality\">{labels.quality} (1-100)</Label>\n <Input\n id=\"style-quality\"\n type=\"number\"\n value={formQuality}\n onChange={(e) => setFormQuality(e.target.value)}\n min={1}\n max={100}\n />\n </div>\n </div>\n\n <DialogFooter>\n <Button variant=\"outline\" type=\"button\" onClick={() => setIsDialogOpen(false)}>\n {labels.cancel}\n </Button>\n <Button type=\"button\" onClick={handleSaveStyle}>\n {labels.save}\n </Button>\n </DialogFooter>\n </DialogContent>\n </Dialog>\n\n <Dialog open={isDeleteOpen} onOpenChange={setIsDeleteOpen}>\n <DialogContent>\n <DialogHeader>\n <DialogTitle>{labels.deleteConfirmTitle}</DialogTitle>\n <DialogDescription>\n {labels.deleteConfirmDescription}\n {deleteKey && (\n <span className=\"block mt-2 font-mono text-foreground\">{deleteKey}</span>\n )}\n </DialogDescription>\n </DialogHeader>\n <DialogFooter>\n <Button variant=\"outline\" type=\"button\" onClick={() => setIsDeleteOpen(false)}>\n {labels.cancel}\n </Button>\n <Button variant=\"destructive\" type=\"button\" onClick={handleDelete}>\n {labels.delete}\n </Button>\n </DialogFooter>\n </DialogContent>\n </Dialog>\n </div>\n )\n}\n","'use client'\n\nimport type { ImageShape, ImageVocabulary } from '../types.js'\nimport { Button, cn, Input, Label } from '@murumets-ee/ui'\nimport { Plus, Trash2 } from 'lucide-react'\nimport { useTranslations } from 'next-intl'\nimport { type KeyboardEvent, useId, useRef, useState } from 'react'\nimport {\n DEFAULT_IMAGE_VOCABULARY,\n MAX_LADDER_RUNGS,\n MAX_SHAPES,\n MAX_VARIANT_WIDTH,\n MIN_VARIANT_WIDTH,\n NATURAL_SHAPE,\n parseShapeRatio,\n plannedFileCount,\n SHAPE_NAME_PATTERN,\n} from '../shapes.js'\nimport { MAX_VARIANTS_PER_IMAGE } from '../variant-plan.js'\nimport type { ImageVocabularyEditorProps } from './types.js'\n\n/**\n * Every visible string, as a next-intl KEY plus the English fallback — the\n * interface-copy rule. The keys live in this package's catalogue\n * (`src/messages/*.json`, `Media.imageVocabulary.*`).\n */\nconst COPY = {\n description: [\n 'Media.imageVocabulary.description',\n 'Every photograph is generated in each shape below, at each width. A slot whose ratio matches a shape draws that shape’s files; a slot with any other ratio is cropped in the browser instead.',\n ],\n usingDefaults: [\n 'Media.imageVocabulary.usingDefaults',\n 'Nothing is saved here yet. Shown are the toolkit defaults; until you save, the site uses them — or the shapes configured in its code, if it has any.',\n ],\n widthsLabel: ['Media.imageVocabulary.widthsLabel', 'Widths (px)'],\n widthsHelp: [\n 'Media.imageVocabulary.widthsHelp',\n 'Ascending, separated by commas. A photograph is never enlarged — a width it cannot supply is skipped.',\n ],\n widthsInvalid: [\n 'Media.imageVocabulary.widthsInvalid',\n 'Use up to {count} ascending whole numbers between {min} and {max}.',\n ],\n shapesLabel: ['Media.imageVocabulary.shapesLabel', 'Shapes'],\n naturalNote: [\n 'Media.imageVocabulary.naturalNote',\n 'Plus “natural” — each photograph at its own shape, over the same widths.',\n ],\n columnName: ['Media.imageVocabulary.columnName', 'Name'],\n columnRatio: ['Media.imageVocabulary.columnRatio', 'Ratio (W:H)'],\n columnWidths: ['Media.imageVocabulary.columnWidths', 'Own widths'],\n ownWidthsPlaceholder: ['Media.imageVocabulary.ownWidthsPlaceholder', 'All widths'],\n ratioInvalid: ['Media.imageVocabulary.ratioInvalid', 'Write the ratio as W:H, for example 3:2.'],\n nameInvalid: [\n 'Media.imageVocabulary.nameInvalid',\n 'Lowercase letters, digits, - and _, starting with a letter — and not “natural”.',\n ],\n nameTaken: ['Media.imageVocabulary.nameTaken', 'A shape named “{name}” already exists.'],\n tooMany: ['Media.imageVocabulary.tooMany', 'At most {count} shapes.'],\n tooManyFiles: [\n 'Media.imageVocabulary.tooManyFiles',\n 'That makes {count} files for every photograph; the most is {max}. Remove a shape or a width first.',\n ],\n add: ['Media.imageVocabulary.add', 'Add shape'],\n remove: ['Media.imageVocabulary.remove', 'Remove {name}'],\n} as const satisfies Record<string, readonly [string, string]>\n\n/** Every catalogue key this component reads — exported for the catalogue test. */\nexport const IMAGE_VOCABULARY_MESSAGE_KEYS: readonly string[] = Object.values(COPY).map(([k]) => k)\n\ntype CopyId = keyof typeof COPY\ntype CopyValues = Record<string, string | number>\n\nfunction interpolate(template: string, values: CopyValues | undefined): string {\n if (!values) return template\n return template.replace(/\\{(\\w+)\\}/g, (whole, name: string) =>\n name in values ? String(values[name]) : whole,\n )\n}\n\n/**\n * Resolve copy with an UNSCOPED translator, falling back to the English LABEL\n * — never to the key — when the app has not merged this package's catalogue.\n */\nfunction useCopy(): (id: CopyId, values?: CopyValues) => string {\n const t = useTranslations()\n return (id, values) => {\n const [key, fallback] = COPY[id]\n return t.has(key) ? t(key, values) : interpolate(fallback, values)\n }\n}\n\n/** `'320, 640,960'` → `[320, 640, 960]`, or `null` unless ascending, distinct, in bounds. */\nexport function parseWidthList(raw: string): number[] | null {\n const parts = raw\n .split(',')\n .map((p) => p.trim())\n .filter((p) => p !== '')\n if (parts.length === 0 || parts.length > MAX_LADDER_RUNGS) return null\n const widths: number[] = []\n for (const part of parts) {\n if (!/^\\d+$/.test(part)) return null\n const w = Number(part)\n if (w < MIN_VARIANT_WIDTH || w > MAX_VARIANT_WIDTH) return null\n const previous = widths[widths.length - 1]\n if (previous !== undefined && w <= previous) return null\n widths.push(w)\n }\n return widths\n}\n\nfunction sameWidths(a: readonly number[], b: readonly number[]): boolean {\n return a.length === b.length && a.every((w, i) => w === b[i])\n}\n\nfunction formatWidths(widths: readonly number[] | undefined): string {\n return widths ? widths.join(', ') : ''\n}\n\n/**\n * Enter commits the field it was pressed in, and never submits the settings\n * form: fields commit on blur, so a form submitted from inside one would save\n * the value from BEFORE the edit and report success.\n */\nfunction onEnter(commit: () => void) {\n return (event: KeyboardEvent<HTMLInputElement>) => {\n // Enter that CONFIRMS an IME candidate is not a commit — Chrome flags it\n // `isComposing`, Safari sends keyCode 229.\n if (event.key !== 'Enter' || event.nativeEvent.isComposing || event.keyCode === 229) return\n event.preventDefault()\n commit()\n }\n}\n\n/** The seeded vocabulary as an editable value — the shared constant is frozen. */\nfunction copyOfDefaults(): ImageVocabulary {\n return {\n shapes: Object.fromEntries(\n Object.entries(DEFAULT_IMAGE_VOCABULARY.shapes).map(([name, shape]) => [name, { ...shape }]),\n ),\n ladder: [...DEFAULT_IMAGE_VOCABULARY.ladder],\n }\n}\n\n/**\n * The editor for `media.imageStyles` → `vocabulary`: the shape vocabulary and\n * the width ladder (images D003). Controlled — every valid edit commits through\n * `onChange`, and the settings form's Save persists it; an invalid one stays in\n * its own field with the reason beside it and commits nothing.\n *\n * `value` is `null` until something is saved, because the key deliberately\n * has no settings default (so the plugin-config tier stays reachable). The\n * editor then shows the seeded vocabulary and says so.\n */\nexport function ImageVocabularyEditor(props: ImageVocabularyEditorProps) {\n // The fields keep DRAFTS (a half-typed ratio must not be thrown away), so an\n // EXTERNAL change to `value` — a form reset, a reload — has to re-seed them.\n // Our own commits must not: remounting on those would drop focus from the\n // field the operator just tabbed into. So the fields remount on a value this\n // editor did not produce, tracked during render rather than in an effect.\n const serialized = JSON.stringify(props.value ?? null)\n const [committed, setCommitted] = useState<string | null>(null)\n const [seen, setSeen] = useState(serialized)\n const [epoch, setEpoch] = useState(0)\n if (serialized !== seen) {\n setSeen(serialized)\n if (serialized !== committed) setEpoch(epoch + 1)\n }\n const onChange = (next: ImageVocabulary) => {\n setCommitted(JSON.stringify(next))\n props.onChange(next)\n }\n return <VocabularyFields key={epoch} {...props} onChange={onChange} />\n}\n\nfunction VocabularyFields({ value, onChange, error, className }: ImageVocabularyEditorProps) {\n const copy = useCopy()\n const ids = useId()\n const vocabulary: ImageVocabulary = value ?? copyOfDefaults()\n const names = Object.keys(vocabulary.shapes).sort()\n\n const [ladderDraft, setLadderDraft] = useState(formatWidths(vocabulary.ladder))\n const [ladderError, setLadderError] = useState(false)\n const [newName, setNewName] = useState('')\n const [newRatio, setNewRatio] = useState('')\n const [addError, setAddError] = useState<string | null>(null)\n const [capError, setCapError] = useState<string | null>(null)\n const ratioInput = useRef<HTMLInputElement>(null)\n\n /**\n * Commit, unless the result plans more files per photograph than the cap —\n * the settings route would refuse it on Save, so refuse it here, where the\n * operator is looking, instead.\n */\n const commit = (next: ImageVocabulary): boolean => {\n const count = plannedFileCount(next)\n if (count > MAX_VARIANTS_PER_IMAGE) {\n setCapError(copy('tooManyFiles', { count, max: MAX_VARIANTS_PER_IMAGE }))\n return false\n }\n setCapError(null)\n onChange(next)\n return true\n }\n\n const commitLadder = () => {\n const parsed = parseWidthList(ladderDraft)\n setLadderError(parsed === null)\n // Blurring an untouched field must commit NOTHING: with no stored value the\n // editor is showing the defaults, and committing them would make the next\n // Save store them — silently shadowing a vocabulary configured in code.\n if (parsed && !sameWidths(parsed, vocabulary.ladder)) commit({ ...vocabulary, ladder: parsed })\n }\n\n const addShape = () => {\n const name = newName.trim()\n const ratio = newRatio.trim()\n if (!SHAPE_NAME_PATTERN.test(name) || name === NATURAL_SHAPE) {\n setAddError(copy('nameInvalid'))\n return\n }\n // `hasOwn`: `constructor` passes the name rule, and a plain lookup finds\n // `Object.prototype.constructor` — \"taken\" for a name nobody used.\n if (Object.hasOwn(vocabulary.shapes, name)) {\n setAddError(copy('nameTaken', { name }))\n return\n }\n if (names.length >= MAX_SHAPES) {\n setAddError(copy('tooMany', { count: MAX_SHAPES }))\n return\n }\n if (parseShapeRatio(ratio) === null) {\n setAddError(copy('ratioInvalid'))\n return\n }\n setAddError(null)\n if (commit({ ...vocabulary, shapes: { ...vocabulary.shapes, [name]: { ratio } } })) {\n setNewName('')\n setNewRatio('')\n }\n }\n\n const replaceShape = (name: string, shape: ImageShape) =>\n commit({ ...vocabulary, shapes: { ...vocabulary.shapes, [name]: shape } })\n\n const removeShape = (name: string) => {\n const { [name]: _removed, ...rest } = vocabulary.shapes\n commit({ ...vocabulary, shapes: rest })\n }\n\n return (\n <div className={cn('space-y-6', className)}>\n <div className=\"space-y-1 text-sm text-muted-foreground\">\n <p>{copy('description')}</p>\n {value === null || value === undefined ? <p>{copy('usingDefaults')}</p> : null}\n </div>\n\n {capError ? (\n <p role=\"alert\" className=\"text-sm text-destructive\">\n {capError}\n </p>\n ) : null}\n\n {error ? (\n <div className=\"rounded-md border border-destructive/50 bg-destructive/10 px-4 py-3 text-sm text-destructive\">\n {error}\n </div>\n ) : null}\n\n <div className=\"space-y-2\">\n <Label htmlFor={`${ids}-ladder`}>{copy('widthsLabel')}</Label>\n <Input\n id={`${ids}-ladder`}\n value={ladderDraft}\n onChange={(e) => setLadderDraft(e.target.value)}\n onBlur={commitLadder}\n onKeyDown={onEnter(commitLadder)}\n aria-invalid={ladderError}\n aria-describedby={`${ids}-ladder-help`}\n inputMode=\"numeric\"\n />\n <p\n id={`${ids}-ladder-help`}\n className={cn('text-xs', ladderError ? 'text-destructive' : 'text-muted-foreground')}\n >\n {ladderError\n ? copy('widthsInvalid', {\n count: MAX_LADDER_RUNGS,\n min: MIN_VARIANT_WIDTH,\n max: MAX_VARIANT_WIDTH,\n })\n : copy('widthsHelp')}\n </p>\n </div>\n\n <fieldset className=\"space-y-2\">\n <legend className=\"text-sm font-medium\">{copy('shapesLabel')}</legend>\n <div className=\"grid grid-cols-[minmax(6rem,1fr)_8rem_minmax(8rem,2fr)_2.5rem] gap-2 text-xs text-muted-foreground\">\n <span>{copy('columnName')}</span>\n <span>{copy('columnRatio')}</span>\n <span>{copy('columnWidths')}</span>\n <span />\n </div>\n {names.map((name) => {\n const shape = vocabulary.shapes[name]\n return shape ? (\n <ShapeRow\n key={name}\n name={name}\n shape={shape}\n copy={copy}\n onReplace={(next) => replaceShape(name, next)}\n onRemove={() => removeShape(name)}\n />\n ) : null\n })}\n <p className=\"text-xs text-muted-foreground\">{copy('naturalNote')}</p>\n\n <div className=\"grid grid-cols-[minmax(6rem,1fr)_8rem_auto] items-start gap-2 pt-2\">\n <Input\n aria-label={copy('columnName')}\n value={newName}\n onChange={(e) => setNewName(e.target.value)}\n onKeyDown={onEnter(() => ratioInput.current?.focus())}\n />\n <Input\n ref={ratioInput}\n aria-label={copy('columnRatio')}\n value={newRatio}\n onChange={(e) => setNewRatio(e.target.value)}\n onKeyDown={onEnter(addShape)}\n placeholder=\"3:2\"\n />\n <Button type=\"button\" size=\"sm\" onClick={addShape}>\n <Plus className=\"mr-1 h-4 w-4\" />\n {copy('add')}\n </Button>\n </div>\n {addError ? (\n <p role=\"alert\" className=\"text-xs text-destructive\">\n {addError}\n </p>\n ) : null}\n </fieldset>\n </div>\n )\n}\n\nfunction ShapeRow({\n name,\n shape,\n copy,\n onReplace,\n onRemove,\n}: {\n name: string\n shape: ImageShape\n copy: ReturnType<typeof useCopy>\n onReplace: (next: ImageShape) => boolean\n onRemove: () => void\n}) {\n const [ratio, setRatio] = useState(shape.ratio)\n const [widths, setWidths] = useState(formatWidths(shape.widths))\n const [problem, setProblem] = useState<CopyId | null>(null)\n\n const commitRatio = () => {\n const ok = parseShapeRatio(ratio.trim()) !== null\n setProblem(ok ? null : 'ratioInvalid')\n if (ok && ratio.trim() !== shape.ratio) onReplace({ ...shape, ratio: ratio.trim() })\n }\n\n const commitWidths = () => {\n if (widths.trim() === '') {\n setProblem(null)\n const { widths: _dropped, ...rest } = shape\n if (shape.widths) onReplace(rest)\n return\n }\n const parsed = parseWidthList(widths)\n setProblem(parsed ? null : 'widthsInvalid')\n if (parsed && !sameWidths(parsed, shape.widths ?? [])) onReplace({ ...shape, widths: parsed })\n }\n\n return (\n <div className=\"space-y-1\">\n <div className=\"grid grid-cols-[minmax(6rem,1fr)_8rem_minmax(8rem,2fr)_2.5rem] items-center gap-2\">\n <span className=\"font-mono text-sm\">{name}</span>\n <Input\n aria-label={`${name} ${copy('columnRatio')}`}\n value={ratio}\n onChange={(e) => setRatio(e.target.value)}\n onBlur={commitRatio}\n onKeyDown={onEnter(commitRatio)}\n aria-invalid={problem === 'ratioInvalid'}\n />\n <Input\n aria-label={`${name} ${copy('columnWidths')}`}\n value={widths}\n onChange={(e) => setWidths(e.target.value)}\n onBlur={commitWidths}\n onKeyDown={onEnter(commitWidths)}\n placeholder={copy('ownWidthsPlaceholder')}\n aria-invalid={problem === 'widthsInvalid'}\n />\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n onClick={onRemove}\n title={copy('remove', { name })}\n aria-label={copy('remove', { name })}\n className=\"text-destructive hover:text-destructive\"\n >\n <Trash2 className=\"h-3.5 w-3.5\" />\n </Button>\n </div>\n {problem ? (\n <p role=\"alert\" className=\"text-xs text-destructive\">\n {copy(problem, {\n count: MAX_LADDER_RUNGS,\n min: MIN_VARIANT_WIDTH,\n max: MAX_VARIANT_WIDTH,\n })}\n </p>\n ) : null}\n </div>\n )\n}\n","'use client'\n\nimport { Button } from '@murumets-ee/ui'\nimport { Loader2, RefreshCw } from 'lucide-react'\nimport { useTranslations } from 'next-intl'\nimport { useCallback, useState } from 'react'\nimport type { RegenerateVariantsActionProps } from './types.js'\n\n/**\n * Every visible string, as a next-intl KEY plus the English fallback — the\n * interface-copy rule. Keys live in `src/messages/*.json` (`Media.variantGeneration.*`).\n */\nconst COPY = {\n workerRequired: [\n 'Media.variantGeneration.workerRequired',\n 'Image sizes are generated in the background by the queue worker — a separate process that has to run beside the site (pnpm worker). Without it, the site still works, but new and changed images are shown at full size.',\n ],\n regenerate: ['Media.variantGeneration.regenerate', 'Catch up all images'],\n regenerating: ['Media.variantGeneration.regenerating', 'Starting…'],\n queued: [\n 'Media.variantGeneration.queued',\n 'Started. The queue worker catches the library up in the background; the site keeps serving meanwhile. Follow it on the Queue page.',\n ],\n alreadyRunning: [\n 'Media.variantGeneration.alreadyRunning',\n 'A catch-up is already running — follow it on the Queue page.',\n ],\n inlineDone: [\n 'Media.variantGeneration.inlineDone',\n 'Processed {processed} of {total}; skipped {skipped}; errors {errors}.',\n ],\n failed: ['Media.variantGeneration.failed', 'Could not start the catch-up: {reason}'],\n} as const satisfies Record<string, readonly [string, string]>\n\n/** Every catalogue key this component reads — exported for the catalogue test. */\nexport const REGENERATE_VARIANTS_MESSAGE_KEYS: readonly string[] = Object.values(COPY).map(\n ([k]) => k,\n)\n\ntype CopyId = keyof typeof COPY\ntype CopyValues = Record<string, string | number>\n\nfunction interpolate(template: string, values: CopyValues | undefined): string {\n if (!values) return template\n return template.replace(/\\{(\\w+)\\}/g, (whole, name: string) =>\n name in values ? String(values[name]) : whole,\n )\n}\n\n/** What `POST /media/regenerate-variants` answers. */\ntype RegenerateResponse =\n | { mode: 'queued'; jobId: string; alreadyRunning: boolean }\n | { mode: 'inline'; total: number; processed: number; skipped: number; errors: number }\n\ntype Outcome =\n | { kind: 'queued'; alreadyRunning: boolean }\n | { kind: 'inline'; total: number; processed: number; skipped: number; errors: number }\n | { kind: 'error'; reason: string }\n\n/**\n * The header action on `/admin/settings/media.imageStyles`.\n *\n * Says, always and before anyone clicks, that sizes come from the queue\n * worker — an operator who has never heard of the worker must learn it here,\n * not from a slow page (images C6). The button then asks the server to catch\n * the library up: with a queue that QUEUES a background backfill and returns at\n * once (202); only an app with no queue runs it inside the request, as before.\n */\nexport function RegenerateVariantsAction({\n apiBasePath,\n labels: userLabels,\n}: RegenerateVariantsActionProps) {\n const t = useTranslations()\n const copy = (id: CopyId, values?: CopyValues): string => {\n const [key, fallback] = COPY[id]\n return t.has(key) ? t(key, values) : interpolate(fallback, values)\n }\n const [isRunning, setIsRunning] = useState(false)\n const [outcome, setOutcome] = useState<Outcome | null>(null)\n\n const handleClick = useCallback(async () => {\n setIsRunning(true)\n setOutcome(null)\n try {\n const res = await fetch(`${apiBasePath}/media/regenerate-variants`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({}),\n })\n if (!res.ok) {\n const data = (await res.json().catch(() => ({}))) as { error?: string }\n setOutcome({ kind: 'error', reason: data.error ?? String(res.status) })\n return\n }\n const data = (await res.json()) as RegenerateResponse\n setOutcome(\n data.mode === 'queued'\n ? { kind: 'queued', alreadyRunning: data.alreadyRunning }\n : { kind: 'inline', ...data },\n )\n } catch (err) {\n setOutcome({ kind: 'error', reason: err instanceof Error ? err.message : String(err) })\n } finally {\n setIsRunning(false)\n }\n }, [apiBasePath])\n\n return (\n <div className=\"flex max-w-xl flex-col items-end gap-2\">\n <p className=\"text-right text-xs text-muted-foreground\" data-variant-worker-required=\"\">\n {copy('workerRequired')}\n </p>\n <div className=\"flex items-center gap-3\">\n {outcome?.kind === 'queued' && (\n <span className=\"text-xs text-muted-foreground\" role=\"status\">\n {copy(outcome.alreadyRunning ? 'alreadyRunning' : 'queued')}\n </span>\n )}\n {outcome?.kind === 'inline' && (\n <span className=\"text-xs text-muted-foreground\" role=\"status\">\n {copy('inlineDone', {\n processed: outcome.processed,\n total: outcome.total,\n skipped: outcome.skipped,\n errors: outcome.errors,\n })}\n </span>\n )}\n {outcome?.kind === 'error' && (\n <span className=\"text-xs text-destructive\" role=\"alert\">\n {copy('failed', { reason: outcome.reason })}\n </span>\n )}\n <Button\n variant=\"outline\"\n size=\"sm\"\n type=\"button\"\n onClick={handleClick}\n disabled={isRunning}\n >\n {isRunning ? (\n <>\n <Loader2 className=\"h-4 w-4 mr-1 animate-spin\" />\n {userLabels?.regenerating ?? copy('regenerating')}\n </>\n ) : (\n <>\n <RefreshCw className=\"h-4 w-4 mr-1\" />\n {userLabels?.regenerate ?? copy('regenerate')}\n </>\n )}\n </Button>\n </div>\n </div>\n )\n}\n"],"mappings":"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"}