@jjlmoya/utils-books 1.16.0 → 1.17.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 (36) hide show
  1. package/package.json +1 -1
  2. package/src/category/index.ts +2 -1
  3. package/src/entries.ts +4 -1
  4. package/src/tests/locale_completeness.test.ts +1 -1
  5. package/src/tests/mfe_assets_contract.test.ts +51 -0
  6. package/src/tests/tool_validation.test.ts +1 -1
  7. package/src/tool/book-cover-bleed-calculator/bibliography.astro +16 -0
  8. package/src/tool/book-cover-bleed-calculator/bibliography.ts +15 -0
  9. package/src/tool/book-cover-bleed-calculator/book-cover-bleed-calculator.css +362 -0
  10. package/src/tool/book-cover-bleed-calculator/component.astro +80 -0
  11. package/src/tool/book-cover-bleed-calculator/controller.ts +91 -0
  12. package/src/tool/book-cover-bleed-calculator/dom-views.ts +41 -0
  13. package/src/tool/book-cover-bleed-calculator/entry.ts +27 -0
  14. package/src/tool/book-cover-bleed-calculator/evaluator.ts +18 -0
  15. package/src/tool/book-cover-bleed-calculator/i18n/de.ts +22 -0
  16. package/src/tool/book-cover-bleed-calculator/i18n/en.ts +75 -0
  17. package/src/tool/book-cover-bleed-calculator/i18n/es.ts +22 -0
  18. package/src/tool/book-cover-bleed-calculator/i18n/fr.ts +22 -0
  19. package/src/tool/book-cover-bleed-calculator/i18n/id.ts +22 -0
  20. package/src/tool/book-cover-bleed-calculator/i18n/it.ts +22 -0
  21. package/src/tool/book-cover-bleed-calculator/i18n/ja.ts +22 -0
  22. package/src/tool/book-cover-bleed-calculator/i18n/ko.ts +22 -0
  23. package/src/tool/book-cover-bleed-calculator/i18n/nl.ts +22 -0
  24. package/src/tool/book-cover-bleed-calculator/i18n/pl.ts +22 -0
  25. package/src/tool/book-cover-bleed-calculator/i18n/pt.ts +22 -0
  26. package/src/tool/book-cover-bleed-calculator/i18n/ru.ts +22 -0
  27. package/src/tool/book-cover-bleed-calculator/i18n/sv.ts +22 -0
  28. package/src/tool/book-cover-bleed-calculator/i18n/tr.ts +22 -0
  29. package/src/tool/book-cover-bleed-calculator/i18n/zh.ts +22 -0
  30. package/src/tool/book-cover-bleed-calculator/index.ts +11 -0
  31. package/src/tool/book-cover-bleed-calculator/logic.test.ts +28 -0
  32. package/src/tool/book-cover-bleed-calculator/logic.ts +65 -0
  33. package/src/tool/book-cover-bleed-calculator/seo.astro +15 -0
  34. package/src/tool/book-cover-bleed-calculator/storage.ts +31 -0
  35. package/src/tool/book-cover-bleed-calculator/ui.ts +32 -0
  36. package/src/tools.ts +2 -1
@@ -0,0 +1,22 @@
1
+ import type { ToolLocaleContent } from '../../../types';
2
+ import { bibliography } from '../bibliography';
3
+ import type { BookCoverBleedUI } from '../ui';
4
+
5
+ const ui: BookCoverBleedUI = {
6
+ presetLabel: 'Börja med ett omslagsformat', tradePreset: 'Handelsbok pocket', a5Preset: 'A5 pocket', digestPreset: 'Digest pocket', unitLabel: 'Måttsystem', metricLabel: 'Metriskt mm', imperialLabel: 'Imperial in', trimWidthLabel: 'Skärbredd', trimHeightLabel: 'Skärhöjd', pageCountLabel: 'Inlagans sidor', pageThicknessLabel: 'Papperstjocklek per sida', bleedLabel: 'Utfall vid varje yttre kant', spineSafetyLabel: 'Säkerhet för ryggtext i korrekturet', spineLabel: 'Ryggbredd', fullWidthLabel: 'Omslagets totala bredd', fullHeightLabel: 'Omslagets totala höjd', safeSpineLabel: 'Säker ryggbredd', backLabel: 'Baksida', frontLabel: 'Framsida', spineAreaLabel: 'Mätt rygg', trimLineLabel: 'Skärlinje', bleedZoneLabel: 'Utfall', safetyZoneLabel: 'Säker zon', copyLabel: 'Kopiera omslagsmått', copiedLabel: 'Omslagsmått kopierade', readyLabel: 'Layoutområdet är klart', readyDetail: 'Låt bilden gå ut till utfallet och håll texten inom de säkra zonerna.', narrowSpineLabel: 'Kontrollera ryggen', narrowSpineDetail: 'Ryggen är smalare än standardavståndet för säker text.', invalidLabel: 'Kontrollera omslagsvärdena',
7
+ };
8
+
9
+ const seo: ToolLocaleContent<BookCoverBleedUI>['seo'] = [
10
+ { type: 'title', text: 'Beräkna hela bokomslagets storlek med utfall och rygg', level: 2 },
11
+ { type: 'paragraph', html: 'Förbered ett pocketomslag genom att ange skärbredd, skärhöjd, sidantal, papperstjocklek per sida och utfall. Beräkningen visar rygg, total bredd, total höjd och ett utvikbart korrektur.' },
12
+ { type: 'title', text: 'Vad hela omslagsmåttet innehåller', level: 2 },
13
+ { type: 'paragraph', html: 'Ett helt omslag består av baksida, rygg och framsida i en enda vågrät fil. Bredden består av de två skärpanelerna, den beräknade ryggen och utfall på båda ytterkanterna. Höjden består av skärhöjden och utfall upptill och nedtill.' },
14
+ { type: 'list', items: ['Välj ett format som realistisk utgångspunkt.', 'Byt ut exemplen mot uppgifter från tryckeriet.', 'Använd korrekturet för att se skärlinje, rygg, utfall och säkra zoner.', 'Kopiera hela måttet till dokumentet och jämför med tryckeriets mall.'] },
15
+ { type: 'title', text: 'Läs utfall skärning och säkra zoner', level: 2 },
16
+ { type: 'paragraph', html: 'Utfall förlänger bakgrunder och bilder förbi skärlinjen så att små skärförskjutningar inte ger en vit kant. Skärlinjen visar den färdiga kanten. Den gröna säkra zonen är ett planeringsstöd och ersätter inte tryckeriets toleranser.' },
17
+ { type: 'title', text: 'Använd ryggen som produktionsuppskattning', level: 2 },
18
+ { type: 'paragraph', html: 'Ryggbredden räknas från sidantalet och den angivna papperstjockleken. Papper, bindning, lim och produktionstoleranser kan ändra slutmåttet, så kontrollera alltid den slutliga mallen.' },
19
+ { type: 'tip', title: 'Kontrollera tryckeriets mall före slutlig formgivning', html: 'Mallen kan ändra utfall, falsar, streckkodsområde eller ryggens tolerans. Följ den specifika mallen om den skiljer sig.' },
20
+ ];
21
+
22
+ export const content: ToolLocaleContent<BookCoverBleedUI> = { slug: 'berakna-bokomslag-utfall-rygg', title: 'Kalkylator för utfall och rygg på bokomslag', description: 'Beräkna hela pocketomslaget med skärmått, sidantal, papperstjocklek, rygg och utfall.', ui, seo, faq: [{ question: 'Vad ingår i den totala bredden?', answer: 'Baksida, rygg, framsida och utfall vid båda ytterkanterna.' }, { question: 'Hur beräknas ryggen?', answer: 'Sidantalet multipliceras med papperstjockleken per sida.' }, { question: 'Varför behövs papperstjockleken?', answer: 'Den verkliga ryggbredden beror på papperet i produktionen.' }, { question: 'Är resultatet färdigt för tryck?', answer: 'Det är ett planeringsmått. Jämför alltid med tryckeriets mall.' }], bibliography, howTo: [{ name: 'Välj format', text: 'Välj det preset som ligger närmast din bok.' }, { name: 'Ange måtten', text: 'Ställ in skärning, sidor, papper och utfall.' }, { name: 'Granska korrekturet', text: 'Kontrollera rygg, skärlinjer och säkra zoner.' }, { name: 'Förbered filen', text: 'Använd hela måttet och kontrollera mallen.' }], schemas: [{ '@context': 'https://schema.org', '@type': 'SoftwareApplication', name: 'Kalkylator för utfall och rygg på bokomslag', applicationCategory: 'DesignApplication', operatingSystem: 'Any', offers: { '@type': 'Offer', price: '0', priceCurrency: 'EUR' } }, { '@context': 'https://schema.org', '@type': 'FAQPage', mainEntity: [{ '@type': 'Question', name: 'Vad ingår i den totala bredden?', acceptedAnswer: { '@type': 'Answer', text: 'Baksida, rygg, framsida och utfall.' } }, { '@type': 'Question', name: 'Hur beräknas ryggen?', acceptedAnswer: { '@type': 'Answer', text: 'Sidor multiplicerat med papperstjocklek.' } }] }, { '@context': 'https://schema.org', '@type': 'HowTo', name: 'Beräkna ett komplett bokomslag', step: [{ '@type': 'HowToStep', name: 'Välj format', text: 'Välj ett preset.' }, { '@type': 'HowToStep', name: 'Ange måtten', text: 'Ställ in skärning, sidor, papper och utfall.' }, { '@type': 'HowToStep', name: 'Granska korrekturet', text: 'Kontrollera rygg och säkra zoner.' }, { '@type': 'HowToStep', name: 'Förbered filen', text: 'Använd mått och tryckmall.' }] }] };
@@ -0,0 +1,22 @@
1
+ import type { ToolLocaleContent } from '../../../types';
2
+ import { bibliography } from '../bibliography';
3
+ import type { BookCoverBleedUI } from '../ui';
4
+
5
+ const ui: BookCoverBleedUI = {
6
+ presetLabel: 'Bir kapak formatıyla başlayın', tradePreset: 'Ticari karton kapak', a5Preset: 'A5 karton kapak', digestPreset: 'Digest karton kapak', unitLabel: 'Ölçü sistemi', metricLabel: 'Metrik mm', imperialLabel: 'Imperial in', trimWidthLabel: 'Kesim genişliği', trimHeightLabel: 'Kesim yüksekliği', pageCountLabel: 'İç sayfalar', pageThicknessLabel: 'Sayfa başına kâğıt kalınlığı', bleedLabel: 'Her dış kenarda taşma payı', spineSafetyLabel: 'Önizlemede kullanılan sırt metni güvenliği', spineLabel: 'Sırt genişliği', fullWidthLabel: 'Tam kapak genişliği', fullHeightLabel: 'Tam kapak yüksekliği', safeSpineLabel: 'Güvenli sırt genişliği', backLabel: 'Arka kapak', frontLabel: 'Ön kapak', spineAreaLabel: 'Ölçülen sırt', trimLineLabel: 'Kesim çizgisi', bleedZoneLabel: 'Taşma alanı', safetyZoneLabel: 'Güvenli alan', copyLabel: 'Kapak ölçüsünü kopyala', copiedLabel: 'Kapak ölçüsü kopyalandı', readyLabel: 'Yerleşim alanı hazır', readyDetail: 'Görseli taşma alanına kadar uzatın ve metni güvenli alanlarda tutun.', narrowSpineLabel: 'Sırtı kontrol edin', narrowSpineDetail: 'Sırt, varsayılan metin güvenlik aralığından daha dar.', invalidLabel: 'Kapak değerlerini kontrol edin',
7
+ };
8
+
9
+ const seo: ToolLocaleContent<BookCoverBleedUI>['seo'] = [
10
+ { type: 'title', text: 'Taşma payı ve sırt ile tam kitap kapağı boyutunu hesaplayın', level: 2 },
11
+ { type: 'paragraph', html: 'Kesim genişliği, kesim yüksekliği, sayfa sayısı, sayfa başına kâğıt kalınlığı ve taşma payını girerek karton kapak hazırlayın. Hesaplama sırtı, tam genişliği, tam yüksekliği ve açılmış bir prova görünümünü gösterir.' },
12
+ { type: 'title', text: 'Tam kapak boyutuna neler dahildir', level: 2 },
13
+ { type: 'paragraph', html: 'Açılmış kapak tek bir yatay dosyada arka kapağı, sırtı ve ön kapağı birleştirir. Genişlik iki kesim panelini, hesaplanan sırtı ve iki dış kenardaki taşma payını toplar. Yükseklik kesim ölçüsüne üst ve alt taşmayı ekler.' },
14
+ { type: 'list', items: ['Gerçekçi bir başlangıç için format seçin.', 'Örnekleri matbaanızın verileriyle değiştirin.', 'Prova görünümünde kesim çizgisini, sırtı, taşmayı ve güvenli alanı inceleyin.', 'Tam ölçüleri belgenize kopyalayın ve baskı şablonuyla karşılaştırın.'] },
15
+ { type: 'title', text: 'Taşma kesim ve güvenli alanları okuyun', level: 2 },
16
+ { type: 'paragraph', html: 'Taşma, küçük kesim kaymalarında beyaz kenar oluşmaması için görseli kesim çizgisinin dışına taşır. Kesim çizgisi bitmiş kenarı gösterir. Yeşil güvenli alan planlama içindir ve belirli bir matbaanın toleranslarının yerine geçmez.' },
17
+ { type: 'title', text: 'Sırtı üretim tahmini olarak kullanın', level: 2 },
18
+ { type: 'paragraph', html: 'Sırt genişliği sayfa sayısı ile girilen sayfa kalınlığından hesaplanır. Kâğıt, cilt yöntemi, yapıştırıcı ve üretim toleransları sonucu değiştirebilir. Son şablonu mutlaka doğrulayın.' },
19
+ { type: 'tip', title: 'Son tasarımdan önce matbaa şablonunu kontrol edin', html: 'Şablon taşmayı, katlama yerlerini, barkod alanını veya sırt toleransını değiştirebilir. Fark varsa matbaanın özel şablonunu kullanın.' },
20
+ ];
21
+
22
+ export const content: ToolLocaleContent<BookCoverBleedUI> = { slug: 'kitap-kapagi-tasma-sirt-hesaplayici', title: 'Kitap kapağı taşma ve sırt hesaplayıcı', description: 'Kesim, sayfa sayısı, kâğıt kalınlığı, sırt ve taşma ile tam karton kapak boyutunu hesaplayın.', ui, seo, faq: [{ question: 'Tam genişliğe neler dahildir?', answer: 'Arka kapak, sırt, ön kapak ve iki dış kenardaki taşma.' }, { question: 'Sırt nasıl hesaplanır?', answer: 'İç sayfa sayısı sayfa başına kâğıt kalınlığı ile çarpılır.' }, { question: 'Kâğıt kalınlığı neden gerekir?', answer: 'Gerçek sırt genişliği üretimde kullanılan kâğıda bağlıdır.' }, { question: 'Sonuç baskıya hazır mı?', answer: 'Bu bir planlama ölçüsüdür. Matbaa şablonuyla karşılaştırın.' }], bibliography, howTo: [{ name: 'Format seçin', text: 'Kitabınıza en yakın preset seçin.' }, { name: 'Ölçüleri girin', text: 'Kesim, sayfa, kâğıt ve taşmayı ayarlayın.' }, { name: 'Provayı inceleyin', text: 'Sırtı, kesim çizgilerini ve güvenli alanları kontrol edin.' }, { name: 'Dosyayı hazırlayın', text: 'Tam ölçüleri kullanın ve şablonu doğrulayın.' }], schemas: [{ '@context': 'https://schema.org', '@type': 'SoftwareApplication', name: 'Kitap kapağı taşma ve sırt hesaplayıcı', applicationCategory: 'DesignApplication', operatingSystem: 'Any', offers: { '@type': 'Offer', price: '0', priceCurrency: 'EUR' } }, { '@context': 'https://schema.org', '@type': 'FAQPage', mainEntity: [{ '@type': 'Question', name: 'Tam genişliğe neler dahildir?', acceptedAnswer: { '@type': 'Answer', text: 'Arka kapak, sırt, ön kapak ve taşma.' } }, { '@type': 'Question', name: 'Sırt nasıl hesaplanır?', acceptedAnswer: { '@type': 'Answer', text: 'Sayfalar kâğıt kalınlığı ile çarpılır.' } }] }, { '@context': 'https://schema.org', '@type': 'HowTo', name: 'Tam kitap kapağı hesaplamak', step: [{ '@type': 'HowToStep', name: 'Format seçin', text: 'Bir preset seçin.' }, { '@type': 'HowToStep', name: 'Ölçüleri girin', text: 'Kesim, sayfa, kâğıt ve taşmayı ayarlayın.' }, { '@type': 'HowToStep', name: 'Provayı inceleyin', text: 'Sırtı ve güvenli alanları kontrol edin.' }, { '@type': 'HowToStep', name: 'Dosyayı hazırlayın', text: 'Ölçüleri ve matbaa şablonunu kullanın.' }] }] };
@@ -0,0 +1,22 @@
1
+ import type { ToolLocaleContent } from '../../../types';
2
+ import { bibliography } from '../bibliography';
3
+ import type { BookCoverBleedUI } from '../ui';
4
+
5
+ const ui: BookCoverBleedUI = {
6
+ presetLabel: '从封面格式开始', tradePreset: '常规平装书', a5Preset: 'A5 平装书', digestPreset: '文摘版平装书', unitLabel: '测量单位', metricLabel: '公制 mm', imperialLabel: '英制 in', trimWidthLabel: '成品宽度', trimHeightLabel: '成品高度', pageCountLabel: '内页页数', pageThicknessLabel: '每页纸张厚度', bleedLabel: '每个外侧边的出血', spineSafetyLabel: '预览中使用的书脊文字安全距离', spineLabel: '书脊宽度', fullWidthLabel: '封面总宽度', fullHeightLabel: '封面总高度', safeSpineLabel: '书脊安全宽度', backLabel: '封底', frontLabel: '封面', spineAreaLabel: '计算书脊', trimLineLabel: '裁切线', bleedZoneLabel: '出血区域', safetyZoneLabel: '安全区域', copyLabel: '复制封面尺寸', copiedLabel: '已复制封面尺寸', readyLabel: '版面范围已准备好', readyDetail: '让图像延伸到出血区域,并将文字放在安全区域内。', narrowSpineLabel: '请检查书脊', narrowSpineDetail: '书脊宽度小于默认的文字安全距离。', invalidLabel: '请检查封面数据',
7
+ };
8
+
9
+ const seo: ToolLocaleContent<BookCoverBleedUI>['seo'] = [
10
+ { type: 'title', text: '计算包含出血和书脊的完整书籍封面尺寸', level: 2 },
11
+ { type: 'paragraph', html: '输入成品宽度、成品高度、内页页数、每页纸张厚度和出血,准备平装书封面。计算器会显示书脊、封面总宽度、封面总高度和展开式预览。' },
12
+ { type: 'title', text: '完整封面尺寸包含哪些部分', level: 2 },
13
+ { type: 'paragraph', html: '展开式封面把封底、书脊和封面放在同一个横向文件中。总宽度由两个成品面、计算出的书脊以及左右外侧出血组成。总高度由成品高度和上下出血组成。' },
14
+ { type: 'list', items: ['选择格式,使用合理的起始值。', '用印刷厂提供的纸张数据替换示例值。', '通过预览查看裁切线、书脊、出血和安全区域。', '把完整尺寸复制到文档,并与印刷模板比较。'] },
15
+ { type: 'title', text: '如何查看出血裁切线和安全区域', level: 2 },
16
+ { type: 'paragraph', html: '出血让图像越过裁切线,避免轻微裁切偏差造成白边。裁切线表示成品边缘。绿色安全区域用于规划文字,不代表某家印刷厂的具体容差。' },
17
+ { type: 'title', text: '把书脊作为制作估算使用', level: 2 },
18
+ { type: 'paragraph', html: '书脊宽度由内页页数和输入的每页纸厚计算。纸张、装订方式、胶水和制作容差都可能改变最终尺寸,因此请以最终印刷模板为准。' },
19
+ { type: 'tip', title: '最终设计前检查印刷模板', html: '模板可能改变出血、折线、条形码区域或书脊容差。如果不一致,请使用印刷厂的专用模板。' },
20
+ ];
21
+
22
+ export const content: ToolLocaleContent<BookCoverBleedUI> = { slug: 'book-cover-bleed-calculator', title: '书籍封面出血和书脊计算器', description: '根据成品尺寸、页数、纸张厚度、书脊和出血计算平装书完整封面尺寸。', ui, seo, faq: [{ question: '总宽度包含哪些部分?', answer: '封底、书脊、封面以及左右外侧出血。' }, { question: '书脊如何计算?', answer: '内页页数乘以每页纸张厚度。' }, { question: '为什么需要纸张厚度?', answer: '实际书脊宽度取决于生产所使用的纸张。' }, { question: '结果可以直接印刷吗?', answer: '这是规划尺寸,请始终与印刷模板比较。' }], bibliography, howTo: [{ name: '选择格式', text: '选择最接近书籍的预设。' }, { name: '输入尺寸', text: '设置成品、页数、纸张和出血。' }, { name: '检查预览', text: '查看书脊、裁切线和安全区域。' }, { name: '准备文件', text: '使用完整尺寸并检查模板。' }], schemas: [{ '@context': 'https://schema.org', '@type': 'SoftwareApplication', name: '书籍封面出血和书脊计算器', applicationCategory: 'DesignApplication', operatingSystem: 'Any', offers: { '@type': 'Offer', price: '0', priceCurrency: 'CNY' } }, { '@context': 'https://schema.org', '@type': 'FAQPage', mainEntity: [{ '@type': 'Question', name: '总宽度包含哪些部分?', acceptedAnswer: { '@type': 'Answer', text: '包含封底、书脊、封面和出血。' } }, { '@type': 'Question', name: '书脊如何计算?', acceptedAnswer: { '@type': 'Answer', text: '页数乘以纸张厚度。' } }] }, { '@context': 'https://schema.org', '@type': 'HowTo', name: '计算完整书籍封面尺寸', step: [{ '@type': 'HowToStep', name: '选择格式', text: '选择预设。' }, { '@type': 'HowToStep', name: '输入尺寸', text: '设置成品、页数、纸张和出血。' }, { '@type': 'HowToStep', name: '检查预览', text: '查看书脊和安全区域。' }, { '@type': 'HowToStep', name: '准备文件', text: '使用尺寸和印刷模板。' }] }] };
@@ -0,0 +1,11 @@
1
+ import type { ToolDefinition } from '../../types';
2
+ import { bookCoverBleedCalculator } from './entry';
3
+
4
+ export * from './entry';
5
+
6
+ export const BOOK_COVER_BLEED_CALCULATOR_TOOL: ToolDefinition = {
7
+ entry: bookCoverBleedCalculator,
8
+ Component: () => import('./component.astro'),
9
+ SEOComponent: () => import('./seo.astro'),
10
+ BibliographyComponent: () => import('./bibliography.astro'),
11
+ };
@@ -0,0 +1,28 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { calculateCover, fromMillimetres, normalizeInputs, toMillimetres } from './logic';
3
+
4
+ describe('book cover bleed calculator logic', () => {
5
+ it('combines both cover panels, spine, and bleed', () => {
6
+ const result = calculateCover({ trimWidthMm: 152.4, trimHeightMm: 228.6, pageCount: 300, pageThicknessMm: 0.0572, bleedMm: 3.2 });
7
+ expect(result.spineMm).toBeCloseTo(17.16, 2);
8
+ expect(result.fullWidthMm).toBeCloseTo(328.36, 2);
9
+ expect(result.fullHeightMm).toBeCloseTo(235, 2);
10
+ });
11
+
12
+ it('keeps a narrow spine from producing a negative safe zone', () => {
13
+ expect(calculateCover({ pageCount: 10, pageThicknessMm: 0.05 }).safeSpineMm).toBe(0);
14
+ });
15
+
16
+ it('normalizes incomplete or invalid inputs to useful defaults', () => {
17
+ expect(normalizeInputs({ trimWidthMm: -1, pageCount: 4.6, bleedMm: -2 }).pageCount).toBe(5);
18
+ expect(normalizeInputs({ trimWidthMm: -1 }).trimWidthMm).toBe(152.4);
19
+ expect(normalizeInputs({ bleedMm: -2 }).bleedMm).toBe(0);
20
+ });
21
+
22
+ it('converts lengths without changing the physical measurement', () => {
23
+ const inches = fromMillimetres(25.4, 'imperial');
24
+ expect(inches).toBe(1);
25
+ expect(toMillimetres(inches, 'imperial')).toBe(25.4);
26
+ expect(fromMillimetres(25.4, 'metric')).toBe(25.4);
27
+ });
28
+ });
@@ -0,0 +1,65 @@
1
+ export type UnitSystem = 'metric' | 'imperial';
2
+
3
+ export interface CoverInputs {
4
+ trimWidthMm: number;
5
+ trimHeightMm: number;
6
+ pageCount: number;
7
+ pageThicknessMm: number;
8
+ bleedMm: number;
9
+ }
10
+
11
+ export interface CoverResult {
12
+ spineMm: number;
13
+ fullWidthMm: number;
14
+ fullHeightMm: number;
15
+ safeSpineMm: number;
16
+ }
17
+
18
+ export const SPINE_SAFETY_MM = 1.6;
19
+
20
+ const TRADE_PRESET: CoverInputs = { trimWidthMm: 152.4, trimHeightMm: 228.6, pageCount: 300, pageThicknessMm: 0.0572, bleedMm: 3.2 };
21
+
22
+ export const PRESETS: Record<string, CoverInputs> = {
23
+ trade: TRADE_PRESET,
24
+ a5: { trimWidthMm: 148, trimHeightMm: 210, pageCount: 240, pageThicknessMm: 0.0572, bleedMm: 3 },
25
+ digest: { trimWidthMm: 139.7, trimHeightMm: 215.9, pageCount: 220, pageThicknessMm: 0.0572, bleedMm: 3.2 },
26
+ };
27
+
28
+ export const DEFAULT_INPUTS = TRADE_PRESET;
29
+
30
+ function positiveValue(value: number, fallback: number): number {
31
+ return Number.isFinite(value) && value > 0 ? value : fallback;
32
+ }
33
+
34
+ export function normalizeInputs(input: Partial<CoverInputs>): CoverInputs {
35
+ return {
36
+ trimWidthMm: positiveValue(input.trimWidthMm ?? 0, DEFAULT_INPUTS.trimWidthMm),
37
+ trimHeightMm: positiveValue(input.trimHeightMm ?? 0, DEFAULT_INPUTS.trimHeightMm),
38
+ pageCount: Math.round(positiveValue(input.pageCount ?? 0, DEFAULT_INPUTS.pageCount)),
39
+ pageThicknessMm: positiveValue(input.pageThicknessMm ?? 0, DEFAULT_INPUTS.pageThicknessMm),
40
+ bleedMm: Math.max(0, Number.isFinite(input.bleedMm) ? input.bleedMm! : DEFAULT_INPUTS.bleedMm),
41
+ };
42
+ }
43
+
44
+ export function calculateCover(input: Partial<CoverInputs>): CoverResult {
45
+ const normalized = normalizeInputs(input);
46
+ const spineMm = normalized.pageCount * normalized.pageThicknessMm;
47
+ return {
48
+ spineMm,
49
+ fullWidthMm: normalized.trimWidthMm * 2 + spineMm + normalized.bleedMm * 2,
50
+ fullHeightMm: normalized.trimHeightMm + normalized.bleedMm * 2,
51
+ safeSpineMm: Math.max(0, spineMm - SPINE_SAFETY_MM * 2),
52
+ };
53
+ }
54
+
55
+ export function fromMillimetres(valueMm: number, unit: UnitSystem): number {
56
+ return unit === 'metric' ? valueMm : valueMm / 25.4;
57
+ }
58
+
59
+ export function toMillimetres(value: number, unit: UnitSystem): number {
60
+ return unit === 'metric' ? value : value * 25.4;
61
+ }
62
+
63
+ export function unitSuffix(unit: UnitSystem): string {
64
+ return unit === 'metric' ? 'mm' : 'in';
65
+ }
@@ -0,0 +1,15 @@
1
+ ---
2
+ import { SEORenderer } from '@jjlmoya/utils-shared';
3
+ import type { KnownLocale } from '../../types';
4
+ import { bookCoverBleedCalculator } from './entry';
5
+
6
+ interface Props {
7
+ locale: KnownLocale;
8
+ }
9
+
10
+ const { locale } = Astro.props;
11
+ const loader = bookCoverBleedCalculator.i18n[locale] ?? bookCoverBleedCalculator.i18n.en;
12
+ const content = loader ? await loader() : null;
13
+ ---
14
+
15
+ {content ? <SEORenderer content={{ locale, sections: content.seo }} /> : null}
@@ -0,0 +1,31 @@
1
+ import type { CoverInputs, UnitSystem } from './logic';
2
+
3
+ const STORAGE_KEY = 'book-cover-bleed-calculator-state';
4
+
5
+ interface StoredState {
6
+ inputs?: Partial<CoverInputs>;
7
+ unit?: UnitSystem;
8
+ }
9
+
10
+ function readState(): StoredState {
11
+ try {
12
+ const value = window.localStorage.getItem(STORAGE_KEY);
13
+ return value ? JSON.parse(value) as StoredState : {};
14
+ } catch {
15
+ return {};
16
+ }
17
+ }
18
+
19
+ function writeState(state: StoredState): void {
20
+ try {
21
+ window.localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
22
+ } catch {}
23
+ }
24
+
25
+ export function loadStoredState(): StoredState {
26
+ return readState();
27
+ }
28
+
29
+ export function saveStoredState(state: StoredState): void {
30
+ writeState(state);
31
+ }
@@ -0,0 +1,32 @@
1
+ export interface BookCoverBleedUI extends Record<string, string> {
2
+ presetLabel: string;
3
+ tradePreset: string;
4
+ a5Preset: string;
5
+ digestPreset: string;
6
+ unitLabel: string;
7
+ metricLabel: string;
8
+ imperialLabel: string;
9
+ trimWidthLabel: string;
10
+ trimHeightLabel: string;
11
+ pageCountLabel: string;
12
+ pageThicknessLabel: string;
13
+ bleedLabel: string;
14
+ spineSafetyLabel: string;
15
+ spineLabel: string;
16
+ fullWidthLabel: string;
17
+ fullHeightLabel: string;
18
+ safeSpineLabel: string;
19
+ backLabel: string;
20
+ frontLabel: string;
21
+ spineAreaLabel: string;
22
+ trimLineLabel: string;
23
+ bleedZoneLabel: string;
24
+ safetyZoneLabel: string;
25
+ copyLabel: string;
26
+ copiedLabel: string;
27
+ readyLabel: string;
28
+ readyDetail: string;
29
+ narrowSpineLabel: string;
30
+ narrowSpineDetail: string;
31
+ invalidLabel: string;
32
+ }
package/src/tools.ts CHANGED
@@ -6,5 +6,6 @@ import { BOOK_READING_TIME_DEADLINE_PLANNER_TOOL } from './tool/book-reading-tim
6
6
  import { BOOK_INDEX_PAGE_BUDGET_CALCULATOR_TOOL } from './tool/book-index-page-budget-calculator';
7
7
  import { BOOK_ROYALTY_BREAK_EVEN_CALCULATOR_TOOL } from './tool/book-royalty-break-even-calculator';
8
8
  import { BOOK_PRINT_SIGNATURE_CALCULATOR_TOOL } from './tool/book-print-signature-calculator';
9
+ import { BOOK_COVER_BLEED_CALCULATOR_TOOL } from './tool/book-cover-bleed-calculator';
9
10
 
10
- export const ALL_TOOLS: ToolDefinition[] = [BOOK_PAGINATION_AND_SPINE_CALCULATOR_TOOL, BOOK_INTERIOR_MARGIN_AND_GUTTER_PLANNER_TOOL, BOOK_READING_TIME_DEADLINE_PLANNER_TOOL, BOOK_INDEX_PAGE_BUDGET_CALCULATOR_TOOL, BOOK_ROYALTY_BREAK_EVEN_CALCULATOR_TOOL, BOOK_PRINT_SIGNATURE_CALCULATOR_TOOL];
11
+ export const ALL_TOOLS: ToolDefinition[] = [BOOK_PAGINATION_AND_SPINE_CALCULATOR_TOOL, BOOK_INTERIOR_MARGIN_AND_GUTTER_PLANNER_TOOL, BOOK_READING_TIME_DEADLINE_PLANNER_TOOL, BOOK_INDEX_PAGE_BUDGET_CALCULATOR_TOOL, BOOK_ROYALTY_BREAK_EVEN_CALCULATOR_TOOL, BOOK_PRINT_SIGNATURE_CALCULATOR_TOOL, BOOK_COVER_BLEED_CALCULATOR_TOOL];