diff --git a/apps/mana/apps/web/src/lib/modules/wardrobe/components/GarmentTryOnButton.svelte b/apps/mana/apps/web/src/lib/modules/wardrobe/components/GarmentTryOnButton.svelte index 7a68d21e3..c7614e769 100644 --- a/apps/mana/apps/web/src/lib/modules/wardrobe/components/GarmentTryOnButton.svelte +++ b/apps/mana/apps/web/src/lib/modules/wardrobe/components/GarmentTryOnButton.svelte @@ -167,16 +167,21 @@ type="button" onclick={handleClick} disabled={running || !canTryOn} - class="flex w-full items-center justify-center gap-2 rounded-md bg-primary px-4 py-2.5 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90 disabled:cursor-not-allowed disabled:opacity-50" + class="flex w-full flex-col items-center justify-center gap-0.5 rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90 disabled:cursor-not-allowed disabled:opacity-50" > {#if running} -
- Rendere… + + + Rendere… + {:else} - - An mir anprobieren · {estimatedCredits} Credits + + + An mir anprobieren + + {estimatedCredits} Credits {/if} diff --git a/apps/mana/apps/web/src/lib/modules/wardrobe/components/TryOnButton.svelte b/apps/mana/apps/web/src/lib/modules/wardrobe/components/TryOnButton.svelte index 3e4e2562c..1f210734f 100644 --- a/apps/mana/apps/web/src/lib/modules/wardrobe/components/TryOnButton.svelte +++ b/apps/mana/apps/web/src/lib/modules/wardrobe/components/TryOnButton.svelte @@ -164,16 +164,21 @@ type="button" onclick={handleClick} disabled={running || !canTryOn} - class="flex w-full items-center justify-center gap-2 rounded-md bg-primary px-4 py-2.5 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90 disabled:cursor-not-allowed disabled:opacity-50" + class="flex w-full flex-col items-center justify-center gap-0.5 rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90 disabled:cursor-not-allowed disabled:opacity-50" > {#if running} -
- Rendere… + + + Rendere… + {:else} - - Anprobieren · {estimatedCredits} Credits + + + Anprobieren + + {estimatedCredits} Credits {/if} diff --git a/apps/mana/apps/web/src/lib/modules/wardrobe/utils/name.ts b/apps/mana/apps/web/src/lib/modules/wardrobe/utils/name.ts new file mode 100644 index 000000000..ad3470668 --- /dev/null +++ b/apps/mana/apps/web/src/lib/modules/wardrobe/utils/name.ts @@ -0,0 +1,73 @@ +/** + * Turn an upload filename into a presentable default garment name. + * + * Filenames from e-commerce sources and phone cameras come in as + * URL-safe slugs with SKU-numbers, duplicate segments, and hyphens — + * e.g. `17390-gestreiftes-herren-t-shirt-aus-baumwolle-17390-2-w.png`. + * A raw strip-extension leaves the user staring at that string as the + * display name and having to manually clean it up. This helper does + * a best-effort pretty-print so the default label is usable as-is. + * + * Rules (order matters): + * 1. Strip the last extension. + * 2. Replace underscores + hyphens with spaces. + * 3. Collapse runs of whitespace. + * 4. Drop "pure-number" tokens that look like SKU / size codes + * (≥ 4 digits AND longer than any letter-only neighbour — matches + * `17390`, `2-w` stays because it's not pure digits). The short + * alpha-numerics like `4xl` / `w38` are kept; stock codes are not. + * 5. Title-case each remaining word. "T-Shirt" style hyphenated terms + * are rebuilt by re-hyphenating two-letter-max fragments so + * `t-shirt` becomes `T-Shirt` and `v-neck` becomes `V-Neck`. + * 6. Trim trailing punctuation + clamp to 80 characters on a word + * boundary so wild inputs don't blow up the UI. + * + * Returns a non-empty string — falls back to the trimmed, extension- + * less original when normalisation would otherwise yield "". + */ +export function prettifyUploadName(filename: string): string { + const extIdx = filename.lastIndexOf('.'); + const withoutExt = extIdx > 0 ? filename.slice(0, extIdx) : filename; + + const raw = withoutExt.replace(/[_-]+/g, ' ').replace(/\s+/g, ' ').trim(); + if (!raw) return filename; + + // Token filter: drop pure-digit tokens of length ≥ 4 (SKU-shaped). + const tokens = raw.split(' ').filter((t) => !(t.length >= 4 && /^\d+$/.test(t))); + + const titled = tokens + .map((t) => { + // "t-shirt" would have been split on hyphens earlier, but if a + // caller pre-tokenised with hyphens we stitch them back here. + if (t.includes('-')) { + return t + .split('-') + .map((seg) => capitalise(seg)) + .join('-'); + } + return capitalise(t); + }) + .join(' ') + .replace(/[\s.,;:\-]+$/, ''); + + const clamped = clampAtWordBoundary(titled, 80); + return clamped || withoutExt; +} + +function capitalise(word: string): string { + if (word.length === 0) return word; + // Keep short tokens that look like codes (`4xl`, `w38`) uppercase + // for readability. Anything ≤ 2 chars or mixed-digit-letter stays + // uppercased so `T-Shirt` works and `w38` reads as `W38`. + if (word.length <= 2 || /[0-9]/.test(word)) { + return word.toUpperCase(); + } + return word[0].toUpperCase() + word.slice(1).toLowerCase(); +} + +function clampAtWordBoundary(s: string, max: number): string { + if (s.length <= max) return s; + const cut = s.slice(0, max); + const lastSpace = cut.lastIndexOf(' '); + return (lastSpace > 0 ? cut.slice(0, lastSpace) : cut).replace(/[\s.,;:\-]+$/, ''); +} diff --git a/apps/mana/apps/web/src/lib/modules/wardrobe/views/DetailGarmentView.svelte b/apps/mana/apps/web/src/lib/modules/wardrobe/views/DetailGarmentView.svelte index 3b1c25794..eb8ba39f2 100644 --- a/apps/mana/apps/web/src/lib/modules/wardrobe/views/DetailGarmentView.svelte +++ b/apps/mana/apps/web/src/lib/modules/wardrobe/views/DetailGarmentView.svelte @@ -6,7 +6,7 @@ --> +
- - {#if !garment} {#if garment$.loading}

Lädt…

@@ -248,7 +240,6 @@

Anproben · {soloTryOns.length}

- Einzelstück auf dir gerendert
{#each soloTryOns as image (image.id)} @@ -282,7 +273,6 @@

In Outfits · {outfits.length}

- Komposition öffnen
{#each outfits as outfit (outfit.id)} diff --git a/apps/mana/apps/web/src/lib/modules/wardrobe/views/GridView.svelte b/apps/mana/apps/web/src/lib/modules/wardrobe/views/GridView.svelte index b14d72a3c..044a36c34 100644 --- a/apps/mana/apps/web/src/lib/modules/wardrobe/views/GridView.svelte +++ b/apps/mana/apps/web/src/lib/modules/wardrobe/views/GridView.svelte @@ -24,6 +24,7 @@ import { CATEGORY_LABELS, CATEGORY_LABELS_SINGULAR } from '../constants'; import CategoryTabs from '../components/CategoryTabs.svelte'; import GarmentCard from '../components/GarmentCard.svelte'; + import { prettifyUploadName } from '../utils/name'; import { getActiveSpace } from '$lib/data/scope'; import type { GarmentCategory } from '../types'; @@ -48,11 +49,6 @@ let uploading = $state(false); let uploadError = $state(null); - function stripExt(filename: string): string { - const i = filename.lastIndexOf('.'); - return i > 0 ? filename.slice(0, i) : filename; - } - async function ingestFiles(files: File[]) { // Pre-select kind from the active tab. Drops on "Alle" land as // 'other' — less specific, user edits on the detail page. Drops @@ -67,7 +63,12 @@ await readImageDimensions(file); const uploaded = await uploadGarmentPhoto(file); await wardrobeGarmentsStore.createGarment({ - name: stripExt(file.name), + // prettifyUploadName turns e-commerce slugs like + // `17390-gestreiftes-herren-t-shirt-aus-baumwolle-17390-2-w` + // into `Gestreiftes Herren-T-Shirt Aus Baumwolle 2-W` so the + // garment row lands with a presentable default. User still + // edits on the detail page for anything nuanced. + name: prettifyUploadName(file.name), category: defaultCategory, mediaIds: [uploaded.mediaId], }); diff --git a/apps/mana/apps/web/src/lib/modules/website/embeds.ts b/apps/mana/apps/web/src/lib/modules/website/embeds.ts index 2e0b1fb2d..4c4753b5c 100644 --- a/apps/mana/apps/web/src/lib/modules/website/embeds.ts +++ b/apps/mana/apps/web/src/lib/modules/website/embeds.ts @@ -552,7 +552,11 @@ async function resolveComicStories(props: ModuleEmbedProps): Promise s.panelImageIds?.[0]) .filter((id): id is string => Boolean(id)); - const coverImages = await db.table('images').where('id').anyOf(coverImageIds).toArray(); + const coverImages = await db + .table('images') + .where('id') + .anyOf(coverImageIds) + .toArray(); const coverById = new Map(); for (const img of coverImages) coverById.set(img.id, img); diff --git a/packages/mana-tool-registry/src/modules/comic.ts b/packages/mana-tool-registry/src/modules/comic.ts index c4184ab84..13208ea2e 100644 --- a/packages/mana-tool-registry/src/modules/comic.ts +++ b/packages/mana-tool-registry/src/modules/comic.ts @@ -159,11 +159,7 @@ export const comicListStories: ToolSpec - decryptRecordFields( - row as unknown as Record, - STORY_ENCRYPTED_FIELDS, - key - ) + decryptRecordFields(row as unknown as Record, STORY_ENCRYPTED_FIELDS, key) ) )) as unknown as RawStoryRow[]; @@ -223,7 +219,7 @@ export const comicCreateStory: ToolSpec c.op !== 'delete' && c.data) .map((c) => c.data as RawStoryRow) - .find( - (row) => - row.id === input.storyId && !row.deletedAt && row.spaceId === ctx.spaceId - ); + .find((row) => row.id === input.storyId && !row.deletedAt && row.spaceId === ctx.spaceId); if (!raw) { throw new Error(`Comic story ${input.storyId} not found in the active space`); }