@jjlmoya/utils-books 1.5.0 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/category/index.ts +2 -1
- package/src/entries.ts +2 -1
- package/src/index.ts +1 -0
- package/src/tests/locale_completeness.test.ts +1 -1
- package/src/tests/seo_translation_completeness.test.ts +1 -1
- package/src/tests/tool_validation.test.ts +1 -1
- package/src/tool/book-interior-margin-and-gutter-planner/bibliography.astro +16 -0
- package/src/tool/book-interior-margin-and-gutter-planner/bibliography.ts +12 -0
- package/src/tool/book-interior-margin-and-gutter-planner/book-interior-margin-and-gutter-planner.css +462 -0
- package/src/tool/book-interior-margin-and-gutter-planner/component.astro +102 -0
- package/src/tool/book-interior-margin-and-gutter-planner/controller.ts +141 -0
- package/src/tool/book-interior-margin-and-gutter-planner/dom-views.ts +66 -0
- package/src/tool/book-interior-margin-and-gutter-planner/entry.ts +27 -0
- package/src/tool/book-interior-margin-and-gutter-planner/evaluator.ts +22 -0
- package/src/tool/book-interior-margin-and-gutter-planner/i18n/de.ts +59 -0
- package/src/tool/book-interior-margin-and-gutter-planner/i18n/en.ts +60 -0
- package/src/tool/book-interior-margin-and-gutter-planner/i18n/es.ts +59 -0
- package/src/tool/book-interior-margin-and-gutter-planner/i18n/fr.ts +59 -0
- package/src/tool/book-interior-margin-and-gutter-planner/i18n/id.ts +59 -0
- package/src/tool/book-interior-margin-and-gutter-planner/i18n/it.ts +59 -0
- package/src/tool/book-interior-margin-and-gutter-planner/i18n/ja.ts +58 -0
- package/src/tool/book-interior-margin-and-gutter-planner/i18n/ko.ts +58 -0
- package/src/tool/book-interior-margin-and-gutter-planner/i18n/nl.ts +59 -0
- package/src/tool/book-interior-margin-and-gutter-planner/i18n/pl.ts +59 -0
- package/src/tool/book-interior-margin-and-gutter-planner/i18n/pt.ts +59 -0
- package/src/tool/book-interior-margin-and-gutter-planner/i18n/ru.ts +58 -0
- package/src/tool/book-interior-margin-and-gutter-planner/i18n/sv.ts +59 -0
- package/src/tool/book-interior-margin-and-gutter-planner/i18n/tr.ts +58 -0
- package/src/tool/book-interior-margin-and-gutter-planner/i18n/zh.ts +58 -0
- package/src/tool/book-interior-margin-and-gutter-planner/index.ts +11 -0
- package/src/tool/book-interior-margin-and-gutter-planner/logic.test.ts +43 -0
- package/src/tool/book-interior-margin-and-gutter-planner/logic.ts +85 -0
- package/src/tool/book-interior-margin-and-gutter-planner/seo.astro +15 -0
- package/src/tool/book-interior-margin-and-gutter-planner/storage.ts +25 -0
- package/src/tool/book-interior-margin-and-gutter-planner/ui.ts +42 -0
- package/src/tools.ts +2 -1
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { ToolLocaleContent } from '../../../types';
|
|
2
|
+
import type { BookInteriorUI } from '../ui';
|
|
3
|
+
import { bibliography } from '../bibliography';
|
|
4
|
+
|
|
5
|
+
const ui: BookInteriorUI = {
|
|
6
|
+
setupLabel: 'Baskı ayarları', pagesLabel: 'İç sayfalar', widthLabel: 'Kesim genişliği', heightLabel: 'Kesim yüksekliği',
|
|
7
|
+
bindingLabel: 'Cilt türü', bindingPerfectLabel: 'Amerikan cilt', bindingHardcoverLabel: 'Sert kapak', bindingSaddleLabel: 'Tel dikişli kitapçık',
|
|
8
|
+
paperLabel: 'Kâğıt türü', paperWhiteLabel: 'Beyaz kuşe olmayan', paperCreamLabel: 'Krem kuşe olmayan', paperCoatedLabel: 'Kuşe kâğıt',
|
|
9
|
+
bleedLabel: 'Tam taşma görseli', bleedOn: 'Dahil', bleedOff: 'Dahil değil', metricLabel: 'Metrik mm', imperialLabel: 'İngiliz inç',
|
|
10
|
+
marginLabel: 'Kenar planı', gutterLabel: 'İç cilt payı', outerLabel: 'Dış güvenli kenar', safeWidthLabel: 'Güvenli metin genişliği', safeHeightLabel: 'Güvenli metin yüksekliği',
|
|
11
|
+
normalizedPagesLabel: 'Düzen sayfa sayısı', pageFormatLabel: 'Referans aralığı', sceneLabel: 'Açık sayfa planı',
|
|
12
|
+
sceneHint: 'Sıcak renkli alan kullanılabilir metin bölgesidir. Ortadaki kıvrım cilt kenarıdır; önemli metni bu bölgeden uzak tutun.',
|
|
13
|
+
statusReady: 'Referans aralığı karşılanıyor', statusLowPages: 'Kısa baskı için kontrol gerekli', statusHighPages: 'Referans aralığının dışında', statusInvalid: 'Kesim ölçüsü çok küçük',
|
|
14
|
+
statusReadyDetail: 'Bu ölçüleri başlangıç düzeni olarak kullanın ve matbaa şablonuyla doğrulayın.',
|
|
15
|
+
statusLowPagesDetail: 'Yayınlanmış kenar tablosu 24 sayfadan başlar. Kitapçık ölçülerini matbaadan isteyin.',
|
|
16
|
+
statusHighPagesDetail: 'Yayınlanmış tablo 828 sayfada biter. Özel kenar ve cilt kontrolü isteyin.',
|
|
17
|
+
statusInvalidDetail: 'Seçilen kesim ölçüsü gerekli kenarlardan sonra pozitif bir güvenli metin alanı bırakmıyor.',
|
|
18
|
+
pageLabel: 'Sayfa', spineLabel: 'Cilt kenarı', safeZoneLabel: 'Güvenli metin alanı', trimLabel: 'Kesim ölçüsü',
|
|
19
|
+
sourceLabel: 'Planlama değerleri matbaa provası değildir. Son şablonu, kâğıdı, cildi ve üretim toleranslarını matbaayla doğrulayın.',
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const faq = [
|
|
23
|
+
{ question: 'İç cilt payı nedir?', answer: 'İç cilt payı, cilt kenarının yanındaki ek kenar boşluğudur. Ciltlendikten sonra okunması zorlaşan bölgeden önemli metinleri ve ayrıntıları uzak tutar.' },
|
|
24
|
+
{ question: 'Sayfa sayısı arttıkça cilt payı neden büyür?', answer: 'Daha kalın bir kitap ciltte daha fazla kâğıt toplar. Bu nedenle referans aralıkları, iç bölüm uzadıkça gerekli en küçük cilt payını artırır.' },
|
|
25
|
+
{ question: 'Tam taşma neyi değiştirir?', answer: 'Tam taşma görsellerin kesim çizgisinin ötesinde fazladan alan bırakmasını sağlar. Bu planlayıcı, metni kesimden daha uzakta tutmak için dış güvenli kenarı da artırır.' },
|
|
26
|
+
{ question: 'Bu değerleri doğrudan matbaaya gönderebilir miyim?', answer: 'Hayır. Bunları düzen planı olarak kullanın. Seçilen ölçü, kâğıt, cilt ve üretime ait matbaa şablonu son kaynaktır.' },
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
const howTo = [
|
|
30
|
+
{ name: 'İç formatı girin', text: 'Kitabın son sayfa sayısını ve kesilmiş genişlik ile yüksekliğini ayarlayın.' },
|
|
31
|
+
{ name: 'Üretim bağlamını seçin', text: 'Cilt türünü, kâğıdı ve iç bölümde tam taşma görseli olup olmadığını seçin.' },
|
|
32
|
+
{ name: 'Açık sayfa planını okuyun', text: 'Cilt payını, dış kenarı ve güvenli metin ölçülerini düzen için başlangıç sınırı olarak kullanın.' },
|
|
33
|
+
{ name: 'Matbaa dosyasını doğrulayın', text: 'Planı matbaanın sağladığı kesin şablon ve üretim toleranslarıyla karşılaştırın.' },
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
const seo: ToolLocaleContent<BookInteriorUI>['seo'] = [
|
|
37
|
+
{ type: 'title', text: 'Kitap İç Kenar ve Cilt Payını Planlayın', level: 2 },
|
|
38
|
+
{ type: 'paragraph', html: 'Sayfa sayısı, kesim ölçüsü, cilt, kâğıt ve taşma seçimini birleştirerek baskıya hazır kitap içi için pratik bir başlangıç noktası belirleyin. Planlayıcı, bu seçimleri iç cilt payına, dış güvenli kenara ve açık sayfadaki kullanılabilir metin alanına dönüştürür.' },
|
|
39
|
+
{ type: 'title', text: 'Kenar Planı Nasıl Çalışır', level: 2 },
|
|
40
|
+
{ type: 'list', items: ['Son iç sayfa sayısını ve kesim ölçüsünü girin, ardından işe uygun cildi seçin.', 'Görseller veya renk kesim kenarını aşıyorsa tam taşmayı açın. Dış güvenli kenar genişler.', 'Cilt payını en küçük iç kenar olarak kullanın ve önemli metinleri, sayfa numaralarını ve tablo ayrıntılarını ortadaki kıvrımdan uzak tutun.', 'Güvenli metin genişliği ve yüksekliğini düzen sınırı olarak kullanın, sonra matbaanın kesin şablonunu uygulayın.'] },
|
|
41
|
+
{ type: 'title', text: 'Sayfa Sayısı Cilt Payını Neden Değiştirir', level: 2 },
|
|
42
|
+
{ type: 'paragraph', html: 'Daha kalın bir kitap cilt kenarına daha fazla kâğıt iter. Buradaki referans aralıkları, 24 ile 150 sayfa için 9,6 mm olan iç kenarı 701 ile 828 sayfa için 22,3 mm seviyesine çıkarır. Ciltli kitaplar çift sayıya, tel dikişli kitapçıklar dördün katına yuvarlanır.' },
|
|
43
|
+
{ type: 'table', headers: ['İç sayfalar', 'İç cilt payı', 'Taşmasız dış kenar'], rows: [['24 ile 150', '9,6 mm', 'en az 6,4 mm'], ['151 ile 300', '12,7 mm', 'en az 6,4 mm'], ['301 ile 500', '15,9 mm', 'en az 6,4 mm'], ['501 ile 700', '19,1 mm', 'en az 6,4 mm'], ['701 ile 828', '22,3 mm', 'en az 6,4 mm']] },
|
|
44
|
+
{ type: 'tip', title: 'Sayı sınırına göre tasarlamayın', html: 'En küçük kenarlar üretim tabanıdır, rahat okuma garantisi değildir. Geniş harfler, sayfa numaraları, çizimler ve cildin açılma biçimi için ek alan bırakın. Matbaanın şablonu genel tahminin önündedir.' },
|
|
45
|
+
{ type: 'title', text: 'Taşma ve Güvenli Alanın Görevi Farklıdır', level: 2 },
|
|
46
|
+
{ type: 'paragraph', html: 'Taşma, görselin kesimden sağ çıkması için fazladan alan verir. Metni kesim veya cilt kenarına yaklaştırmaz. Önemli içeriği güvenli alanda tutun, yalnızca gerçekten kenara ulaşan görselleri uzatın ve dışa aktarmadan önce her açık sayfanın ilk ve son sayfasını inceleyin.' },
|
|
47
|
+
{ type: 'tip', title: 'Sonucu düzen brifi olarak kullanın', html: 'Kesim ölçüsünü, sayfa sayısını, cildi, kâğıdı, cilt payını ve taşma seçimini tasarım dosyanızın yanında kaydedin. Kâğıt veya cilt değişirse eski kenarları sessizce kullanmak yerine brifi yeniden hesaplayın.' },
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
export const content: ToolLocaleContent<BookInteriorUI> = {
|
|
51
|
+
slug: 'kitap-ic-kenar-ve-cilt-payi-planlayici', title: 'Kitap İç Kenar ve Cilt Payı Planlayıcı', description: 'Düzenlemeye başlamadan önce kitap içi için cilt payını, dış güvenli kenarı ve kullanılabilir metin alanını planlayın.',
|
|
52
|
+
ui, seo, faq, bibliography, howTo,
|
|
53
|
+
schemas: [
|
|
54
|
+
{ '@context': 'https://schema.org', '@type': 'SoftwareApplication', name: 'Kitap İç Kenar ve Cilt Payı Planlayıcı', applicationCategory: 'DesignApplication', operatingSystem: 'Any', offers: { '@type': 'Offer', price: '0', priceCurrency: 'USD' } },
|
|
55
|
+
{ '@context': 'https://schema.org', '@type': 'FAQPage', mainEntity: faq.map((item) => ({ '@type': 'Question', name: item.question, acceptedAnswer: { '@type': 'Answer', text: item.answer } })) },
|
|
56
|
+
{ '@context': 'https://schema.org', '@type': 'HowTo', name: 'Kitap iç kenarlarını ve cilt payını planlama', step: howTo.map((item) => ({ '@type': 'HowToStep', name: item.name, text: item.text })) },
|
|
57
|
+
],
|
|
58
|
+
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { ToolLocaleContent } from '../../../types';
|
|
2
|
+
import type { BookInteriorUI } from '../ui';
|
|
3
|
+
import { bibliography } from '../bibliography';
|
|
4
|
+
|
|
5
|
+
const ui: BookInteriorUI = {
|
|
6
|
+
setupLabel: '印刷设置', pagesLabel: '内页页数', widthLabel: '成品宽度', heightLabel: '成品高度',
|
|
7
|
+
bindingLabel: '装订方式', bindingPerfectLabel: '无线胶装', bindingHardcoverLabel: '精装', bindingSaddleLabel: '骑马钉小册子',
|
|
8
|
+
paperLabel: '纸张', paperWhiteLabel: '白色非涂布纸', paperCreamLabel: '奶油色非涂布纸', paperCoatedLabel: '涂布纸',
|
|
9
|
+
bleedLabel: '满版出血图片', bleedOn: '包含', bleedOff: '不包含', metricLabel: '公制 mm', imperialLabel: '英制 in',
|
|
10
|
+
marginLabel: '页边距方案', gutterLabel: '装订侧内边距', outerLabel: '外侧安全边距', safeWidthLabel: '安全文字宽度', safeHeightLabel: '安全文字高度',
|
|
11
|
+
normalizedPagesLabel: '排版页数', pageFormatLabel: '参考范围', sceneLabel: '跨页方案',
|
|
12
|
+
sceneHint: '暖色区域是可用的文字区域。中间折线是装订边,请让重要文字避开这里。',
|
|
13
|
+
statusReady: '在参考范围内', statusLowPages: '少页数需要检查', statusHighPages: '超出参考范围', statusInvalid: '成品尺寸太小',
|
|
14
|
+
statusReadyDetail: '将这些尺寸作为排版起点,并用印刷厂模板进行确认。',
|
|
15
|
+
statusLowPagesDetail: '公开的页边距表从24页开始。请向印刷厂索取小册子规格。',
|
|
16
|
+
statusHighPagesDetail: '公开的表格到828页结束。请要求对页边距和装订进行定制审核。',
|
|
17
|
+
statusInvalidDetail: '所选成品尺寸在应用必要边距后没有留下正值的安全文字区域。',
|
|
18
|
+
pageLabel: '页面', spineLabel: '装订边', safeZoneLabel: '安全文字区域', trimLabel: '成品尺寸',
|
|
19
|
+
sourceLabel: '规划数值不是印刷打样。请与印刷厂确认最终模板、纸张、装订方式和生产公差。',
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const faq = [
|
|
23
|
+
{ question: '什么是装订侧内边距?', answer: '这是装订边旁边增加的边距,可以让重要文字和细节远离装订后跨页中央可能变得难以阅读的区域。' },
|
|
24
|
+
{ question: '为什么页数越多,装订侧内边距越大?', answer: '书越厚,装订处聚集的纸张越多。因此,随着内页变长,参考范围会提高所需的最小装订侧内边距。' },
|
|
25
|
+
{ question: '满版出血会改变什么?', answer: '满版图片需要延伸到成品裁切线之外,以留出额外的图像区域。本规划器也会增加外侧安全边距,让文字离裁切线更远。' },
|
|
26
|
+
{ question: '可以直接把这些数值交给印刷厂吗?', answer: '不可以。请把它们当作排版计划的起点。针对所选尺寸、纸张、装订和生产方式的印刷厂模板才是最终依据。' },
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
const howTo = [
|
|
30
|
+
{ name: '输入内页规格', text: '设置书籍最终页数以及成品宽度和高度。' },
|
|
31
|
+
{ name: '选择生产条件', text: '选择装订方式、纸张,以及内页是否包含满版出血图片。' },
|
|
32
|
+
{ name: '查看跨页方案', text: '把装订侧边距、外侧边距和安全文字尺寸作为排版的起始范围。' },
|
|
33
|
+
{ name: '确认印刷文件', text: '将方案与印刷厂提供的准确模板和生产公差进行比较。' },
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
const seo: ToolLocaleContent<BookInteriorUI>['seo'] = [
|
|
37
|
+
{ type: 'title', text: '排版前规划书籍内页边距和装订侧边距', level: 2 },
|
|
38
|
+
{ type: 'paragraph', html: '结合页数、成品尺寸、装订方式、纸张和出血设置,为书籍内页排版建立实用的起点。本规划器会把这些选择转换为装订侧内边距、外侧安全边距以及跨页可用的文字区域。' },
|
|
39
|
+
{ type: 'title', text: '页边距方案如何工作', level: 2 },
|
|
40
|
+
{ type: 'list', items: ['输入最终内页页数和成品尺寸,然后选择符合项目的装订方式。', '当图片或色块延伸到裁切线之外时打开满版出血,外侧安全边距会更宽。', '把装订侧边距作为最小内侧边距,让重要文字、页码和表格细节远离中央折线。', '把安全文字宽度和高度当作排版范围,然后应用印刷厂提供的准确模板。'] },
|
|
41
|
+
{ type: 'title', text: '页数为什么会改变装订侧边距', level: 2 },
|
|
42
|
+
{ type: 'paragraph', html: '书越厚,越多纸张会挤向装订边。这里的参考范围从24到150页的9.6 mm,逐步增加到701到828页的22.3 mm。胶装和精装书会调整为偶数页,骑马钉小册子会调整为4的倍数。' },
|
|
43
|
+
{ type: 'table', headers: ['内页页数', '装订侧边距', '无出血外侧边距'], rows: [['24到150', '9.6 mm', '至少6.4 mm'], ['151到300', '12.7 mm', '至少6.4 mm'], ['301到500', '15.9 mm', '至少6.4 mm'], ['501到700', '19.1 mm', '至少6.4 mm'], ['701到828', '22.3 mm', '至少6.4 mm']] },
|
|
44
|
+
{ type: 'tip', title: '不要贴着最小数值排版', html: '最小边距是生产下限,并不保证阅读舒适。请为较宽的字形、页码、插图以及装订的开合方式留出额外空间。印刷厂模板优先于通用估算。' },
|
|
45
|
+
{ type: 'title', text: '出血和安全区域的作用不同', level: 2 },
|
|
46
|
+
{ type: 'paragraph', html: '出血让图片或背景延伸到裁切线之外,以承受裁切误差。它不会让文字靠近裁切边或装订边变得安全。将重要内容放在安全区域内,只延伸确实需要铺到边缘的图像,并在导出前检查每一个跨页。' },
|
|
47
|
+
{ type: 'tip', title: '把结果当作排版说明', html: '在设计文件旁记录成品尺寸、页数、装订方式、纸张、装订侧边距和出血选择。如果纸张或装订改变,请重新计算,而不要默默沿用旧边距。' },
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
export const content: ToolLocaleContent<BookInteriorUI> = {
|
|
51
|
+
slug: 'book-interior-margin-and-gutter-planner', title: '书籍内页边距和装订侧边距规划器', description: '在开始排版前,规划书籍内页的装订侧边距、外侧安全边距和可用文字区域。',
|
|
52
|
+
ui, seo, faq, bibliography, howTo,
|
|
53
|
+
schemas: [
|
|
54
|
+
{ '@context': 'https://schema.org', '@type': 'SoftwareApplication', name: '书籍内页边距和装订侧边距规划器', applicationCategory: 'DesignApplication', operatingSystem: 'Any', offers: { '@type': 'Offer', price: '0', priceCurrency: 'USD' } },
|
|
55
|
+
{ '@context': 'https://schema.org', '@type': 'FAQPage', mainEntity: faq.map((item) => ({ '@type': 'Question', name: item.question, acceptedAnswer: { '@type': 'Answer', text: item.answer } })) },
|
|
56
|
+
{ '@context': 'https://schema.org', '@type': 'HowTo', name: '规划书籍内页边距和装订侧边距', step: howTo.map((item) => ({ '@type': 'HowToStep', name: item.name, text: item.text })) },
|
|
57
|
+
],
|
|
58
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ToolDefinition } from '../../types';
|
|
2
|
+
import { bookInteriorMarginAndGutterPlanner } from './entry';
|
|
3
|
+
|
|
4
|
+
export * from './entry';
|
|
5
|
+
|
|
6
|
+
export const BOOK_INTERIOR_MARGIN_AND_GUTTER_PLANNER_TOOL: ToolDefinition = {
|
|
7
|
+
entry: bookInteriorMarginAndGutterPlanner,
|
|
8
|
+
Component: () => import('./component.astro'),
|
|
9
|
+
SEOComponent: () => import('./seo.astro'),
|
|
10
|
+
BibliographyComponent: () => import('./bibliography.astro'),
|
|
11
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { calculateInterior, DEFAULT_INPUTS, normalizeInputs } from './logic';
|
|
3
|
+
|
|
4
|
+
describe('book interior margin and gutter planner', () => {
|
|
5
|
+
it('calculates the 151 to 300 page gutter band without bleed', () => {
|
|
6
|
+
const result = calculateInterior(DEFAULT_INPUTS);
|
|
7
|
+
expect(result.pages).toBe(240);
|
|
8
|
+
expect(result.gutterMm).toBe(12.7);
|
|
9
|
+
expect(result.outerMarginMm).toBe(6.4);
|
|
10
|
+
expect(result.safeWidthMm).toBeCloseTo(132.9);
|
|
11
|
+
expect(result.safeHeightMm).toBeCloseTo(216.2);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('uses the wider outside margin when bleed is enabled', () => {
|
|
15
|
+
const result = calculateInterior({ ...DEFAULT_INPUTS, pages: 120, bleed: true });
|
|
16
|
+
expect(result.gutterMm).toBe(9.6);
|
|
17
|
+
expect(result.outerMarginMm).toBe(9.6);
|
|
18
|
+
expect(result.safeWidthMm).toBeCloseTo(132.8);
|
|
19
|
+
expect(result.safeHeightMm).toBeCloseTo(209.8);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('rounds bound books to even pages and saddle stitched books to four', () => {
|
|
23
|
+
expect(calculateInterior({ ...DEFAULT_INPUTS, pages: 151 }).pages).toBe(152);
|
|
24
|
+
expect(calculateInterior({ ...DEFAULT_INPUTS, pages: 151, binding: 'saddle-stitched' }).pages).toBe(152);
|
|
25
|
+
expect(calculateInterior({ ...DEFAULT_INPUTS, pages: 153, binding: 'saddle-stitched' }).pages).toBe(156);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('flags page counts outside the published reference bands', () => {
|
|
29
|
+
const low = calculateInterior({ ...DEFAULT_INPUTS, pages: 12 });
|
|
30
|
+
const high = calculateInterior({ ...DEFAULT_INPUTS, pages: 900 });
|
|
31
|
+
expect(low.belowReferenceFloor).toBe(true);
|
|
32
|
+
expect(high.aboveReferenceCeiling).toBe(true);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('clamps invalid geometry before calculating the safe zone', () => {
|
|
36
|
+
const inputs = normalizeInputs({ ...DEFAULT_INPUTS, pages: 0, widthMm: 1, heightMm: -4 });
|
|
37
|
+
const result = calculateInterior(inputs);
|
|
38
|
+
expect(inputs.pages).toBe(1);
|
|
39
|
+
expect(inputs.widthMm).toBe(20);
|
|
40
|
+
expect(inputs.heightMm).toBe(20);
|
|
41
|
+
expect(result.invalidGeometry).toBe(false);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
export type Binding = 'perfect-bound' | 'hardcover' | 'saddle-stitched';
|
|
2
|
+
export type PaperType = 'white-uncoated' | 'cream-uncoated' | 'coated';
|
|
3
|
+
|
|
4
|
+
export interface BookInteriorInputs {
|
|
5
|
+
pages: number;
|
|
6
|
+
widthMm: number;
|
|
7
|
+
heightMm: number;
|
|
8
|
+
binding: Binding;
|
|
9
|
+
paperType: PaperType;
|
|
10
|
+
bleed: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface BookInteriorCalculation {
|
|
14
|
+
pages: number;
|
|
15
|
+
gutterMm: number;
|
|
16
|
+
outerMarginMm: number;
|
|
17
|
+
safeWidthMm: number;
|
|
18
|
+
safeHeightMm: number;
|
|
19
|
+
trimWidthMm: number;
|
|
20
|
+
trimHeightMm: number;
|
|
21
|
+
referenceBand: string;
|
|
22
|
+
belowReferenceFloor: boolean;
|
|
23
|
+
aboveReferenceCeiling: boolean;
|
|
24
|
+
invalidGeometry: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const DEFAULT_INPUTS: BookInteriorInputs = {
|
|
28
|
+
pages: 240,
|
|
29
|
+
widthMm: 152,
|
|
30
|
+
heightMm: 229,
|
|
31
|
+
binding: 'perfect-bound',
|
|
32
|
+
paperType: 'cream-uncoated',
|
|
33
|
+
bleed: false,
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const GUTTER_BANDS = [
|
|
37
|
+
{ min: 24, max: 150, gutterMm: 9.6 },
|
|
38
|
+
{ min: 151, max: 300, gutterMm: 12.7 },
|
|
39
|
+
{ min: 301, max: 500, gutterMm: 15.9 },
|
|
40
|
+
{ min: 501, max: 700, gutterMm: 19.1 },
|
|
41
|
+
{ min: 701, max: 828, gutterMm: 22.3 },
|
|
42
|
+
] as const;
|
|
43
|
+
|
|
44
|
+
export function normalizeInputs(inputs: BookInteriorInputs): BookInteriorInputs {
|
|
45
|
+
return {
|
|
46
|
+
...inputs,
|
|
47
|
+
pages: Math.max(1, Math.round(inputs.pages)),
|
|
48
|
+
widthMm: Math.max(20, inputs.widthMm),
|
|
49
|
+
heightMm: Math.max(20, inputs.heightMm),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function calculateInterior(inputs: BookInteriorInputs): BookInteriorCalculation {
|
|
54
|
+
const normalized = normalizeInputs(inputs);
|
|
55
|
+
const pages = normalizePages(normalized.pages, normalized.binding);
|
|
56
|
+
const band = getBand(pages);
|
|
57
|
+
const outerMarginMm = normalized.bleed ? 9.6 : 6.4;
|
|
58
|
+
const safeWidthMm = normalized.widthMm - band.gutterMm - outerMarginMm;
|
|
59
|
+
const safeHeightMm = normalized.heightMm - outerMarginMm * 2;
|
|
60
|
+
return {
|
|
61
|
+
pages,
|
|
62
|
+
gutterMm: band.gutterMm,
|
|
63
|
+
outerMarginMm,
|
|
64
|
+
safeWidthMm,
|
|
65
|
+
safeHeightMm,
|
|
66
|
+
trimWidthMm: normalized.widthMm,
|
|
67
|
+
trimHeightMm: normalized.heightMm,
|
|
68
|
+
referenceBand: `${band.min}-${band.max}`,
|
|
69
|
+
belowReferenceFloor: pages < 24,
|
|
70
|
+
aboveReferenceCeiling: pages > 828,
|
|
71
|
+
invalidGeometry: safeWidthMm <= 0 || safeHeightMm <= 0,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function normalizePages(pages: number, binding: Binding): number {
|
|
76
|
+
const multiple = binding === 'saddle-stitched' ? 4 : 2;
|
|
77
|
+
return Math.ceil(pages / multiple) * multiple;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function getBand(pages: number): { min: number; max: number; gutterMm: number } {
|
|
81
|
+
const found = GUTTER_BANDS.find((band) => pages >= band.min && pages <= band.max);
|
|
82
|
+
if (found) return found;
|
|
83
|
+
if (pages < 24) return GUTTER_BANDS[0];
|
|
84
|
+
return GUTTER_BANDS[GUTTER_BANDS.length - 1] ?? GUTTER_BANDS[0];
|
|
85
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { SEORenderer } from '@jjlmoya/utils-shared';
|
|
3
|
+
import { bookInteriorMarginAndGutterPlanner } from './index';
|
|
4
|
+
import type { KnownLocale } from '../../types';
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
locale?: KnownLocale;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const { locale = 'en' } = Astro.props as Props;
|
|
11
|
+
const loader = bookInteriorMarginAndGutterPlanner.i18n[locale] || bookInteriorMarginAndGutterPlanner.i18n.en;
|
|
12
|
+
const content = await loader?.();
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
{content ? <SEORenderer content={{ locale, sections: content.seo }} /> : null}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { BookInteriorInputs } from './logic';
|
|
2
|
+
|
|
3
|
+
const STORAGE_KEY = 'book-interior-margin-and-gutter-planner-state';
|
|
4
|
+
|
|
5
|
+
export interface StoredBookInteriorState {
|
|
6
|
+
inputs: BookInteriorInputs;
|
|
7
|
+
unit: 'metric' | 'imperial';
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function loadBookInteriorState(fallback: StoredBookInteriorState): StoredBookInteriorState {
|
|
11
|
+
try {
|
|
12
|
+
const raw = localStorage.getItem(STORAGE_KEY);
|
|
13
|
+
if (!raw) return fallback;
|
|
14
|
+
const stored = JSON.parse(raw) as Partial<StoredBookInteriorState>;
|
|
15
|
+
return { ...fallback, ...stored, inputs: { ...fallback.inputs, ...stored.inputs } };
|
|
16
|
+
} catch {
|
|
17
|
+
return fallback;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function saveBookInteriorState(state: StoredBookInteriorState): void {
|
|
22
|
+
try {
|
|
23
|
+
localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
|
|
24
|
+
} catch {}
|
|
25
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export interface BookInteriorUI {
|
|
2
|
+
[key: string]: string;
|
|
3
|
+
setupLabel: string;
|
|
4
|
+
pagesLabel: string;
|
|
5
|
+
widthLabel: string;
|
|
6
|
+
heightLabel: string;
|
|
7
|
+
bindingLabel: string;
|
|
8
|
+
bindingPerfectLabel: string;
|
|
9
|
+
bindingHardcoverLabel: string;
|
|
10
|
+
bindingSaddleLabel: string;
|
|
11
|
+
paperLabel: string;
|
|
12
|
+
paperWhiteLabel: string;
|
|
13
|
+
paperCreamLabel: string;
|
|
14
|
+
paperCoatedLabel: string;
|
|
15
|
+
bleedLabel: string;
|
|
16
|
+
bleedOn: string;
|
|
17
|
+
bleedOff: string;
|
|
18
|
+
metricLabel: string;
|
|
19
|
+
imperialLabel: string;
|
|
20
|
+
marginLabel: string;
|
|
21
|
+
gutterLabel: string;
|
|
22
|
+
outerLabel: string;
|
|
23
|
+
safeWidthLabel: string;
|
|
24
|
+
safeHeightLabel: string;
|
|
25
|
+
normalizedPagesLabel: string;
|
|
26
|
+
pageFormatLabel: string;
|
|
27
|
+
sceneLabel: string;
|
|
28
|
+
sceneHint: string;
|
|
29
|
+
statusReady: string;
|
|
30
|
+
statusLowPages: string;
|
|
31
|
+
statusHighPages: string;
|
|
32
|
+
statusInvalid: string;
|
|
33
|
+
statusReadyDetail: string;
|
|
34
|
+
statusLowPagesDetail: string;
|
|
35
|
+
statusHighPagesDetail: string;
|
|
36
|
+
statusInvalidDetail: string;
|
|
37
|
+
pageLabel: string;
|
|
38
|
+
spineLabel: string;
|
|
39
|
+
safeZoneLabel: string;
|
|
40
|
+
trimLabel: string;
|
|
41
|
+
sourceLabel: string;
|
|
42
|
+
}
|
package/src/tools.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { ALL_ENTRIES } from './entries';
|
|
2
2
|
import type { ToolDefinition } from './types';
|
|
3
3
|
import { BOOK_PAGINATION_AND_SPINE_CALCULATOR_TOOL } from './tool/book-pagination-and-spine-calculator';
|
|
4
|
+
import { BOOK_INTERIOR_MARGIN_AND_GUTTER_PLANNER_TOOL } from './tool/book-interior-margin-and-gutter-planner';
|
|
4
5
|
|
|
5
|
-
export const ALL_TOOLS: ToolDefinition[] = [BOOK_PAGINATION_AND_SPINE_CALCULATOR_TOOL];
|
|
6
|
+
export const ALL_TOOLS: ToolDefinition[] = [BOOK_PAGINATION_AND_SPINE_CALCULATOR_TOOL, BOOK_INTERIOR_MARGIN_AND_GUTTER_PLANNER_TOOL];
|