@jjlmoya/utils-books 1.13.0 → 1.15.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 (61) hide show
  1. package/package.json +1 -1
  2. package/src/category/index.ts +3 -1
  3. package/src/entries.ts +5 -1
  4. package/src/tests/locale_completeness.test.ts +1 -1
  5. package/src/tests/tool_validation.test.ts +1 -1
  6. package/src/tool/book-print-signature-calculator/bibliography.astro +16 -0
  7. package/src/tool/book-print-signature-calculator/bibliography.ts +7 -0
  8. package/src/tool/book-print-signature-calculator/book-print-signature-calculator.css +350 -0
  9. package/src/tool/book-print-signature-calculator/component.astro +38 -0
  10. package/src/tool/book-print-signature-calculator/controller.ts +71 -0
  11. package/src/tool/book-print-signature-calculator/dom-views.ts +70 -0
  12. package/src/tool/book-print-signature-calculator/entry.ts +27 -0
  13. package/src/tool/book-print-signature-calculator/i18n/de.ts +53 -0
  14. package/src/tool/book-print-signature-calculator/i18n/en.ts +85 -0
  15. package/src/tool/book-print-signature-calculator/i18n/es.ts +53 -0
  16. package/src/tool/book-print-signature-calculator/i18n/fr.ts +53 -0
  17. package/src/tool/book-print-signature-calculator/i18n/id.ts +53 -0
  18. package/src/tool/book-print-signature-calculator/i18n/it.ts +53 -0
  19. package/src/tool/book-print-signature-calculator/i18n/ja.ts +53 -0
  20. package/src/tool/book-print-signature-calculator/i18n/ko.ts +53 -0
  21. package/src/tool/book-print-signature-calculator/i18n/nl.ts +53 -0
  22. package/src/tool/book-print-signature-calculator/i18n/pl.ts +53 -0
  23. package/src/tool/book-print-signature-calculator/i18n/pt.ts +53 -0
  24. package/src/tool/book-print-signature-calculator/i18n/ru.ts +53 -0
  25. package/src/tool/book-print-signature-calculator/i18n/sv.ts +53 -0
  26. package/src/tool/book-print-signature-calculator/i18n/tr.ts +53 -0
  27. package/src/tool/book-print-signature-calculator/i18n/zh.ts +53 -0
  28. package/src/tool/book-print-signature-calculator/index.ts +11 -0
  29. package/src/tool/book-print-signature-calculator/logic.test.ts +49 -0
  30. package/src/tool/book-print-signature-calculator/logic.ts +88 -0
  31. package/src/tool/book-print-signature-calculator/seo.astro +16 -0
  32. package/src/tool/book-print-signature-calculator/ui.ts +35 -0
  33. package/src/tool/book-royalty-break-even-calculator/bibliography.astro +6 -0
  34. package/src/tool/book-royalty-break-even-calculator/bibliography.ts +11 -0
  35. package/src/tool/book-royalty-break-even-calculator/book-royalty-break-even-calculator.css +449 -0
  36. package/src/tool/book-royalty-break-even-calculator/component.astro +66 -0
  37. package/src/tool/book-royalty-break-even-calculator/controller.ts +23 -0
  38. package/src/tool/book-royalty-break-even-calculator/dom-views.ts +51 -0
  39. package/src/tool/book-royalty-break-even-calculator/entry.ts +39 -0
  40. package/src/tool/book-royalty-break-even-calculator/i18n/de.ts +2 -0
  41. package/src/tool/book-royalty-break-even-calculator/i18n/en.ts +52 -0
  42. package/src/tool/book-royalty-break-even-calculator/i18n/es.ts +2 -0
  43. package/src/tool/book-royalty-break-even-calculator/i18n/fr.ts +2 -0
  44. package/src/tool/book-royalty-break-even-calculator/i18n/id.ts +2 -0
  45. package/src/tool/book-royalty-break-even-calculator/i18n/it.ts +2 -0
  46. package/src/tool/book-royalty-break-even-calculator/i18n/ja.ts +2 -0
  47. package/src/tool/book-royalty-break-even-calculator/i18n/ko.ts +2 -0
  48. package/src/tool/book-royalty-break-even-calculator/i18n/locales.ts +73 -0
  49. package/src/tool/book-royalty-break-even-calculator/i18n/nl.ts +2 -0
  50. package/src/tool/book-royalty-break-even-calculator/i18n/pl.ts +2 -0
  51. package/src/tool/book-royalty-break-even-calculator/i18n/pt.ts +2 -0
  52. package/src/tool/book-royalty-break-even-calculator/i18n/ru.ts +2 -0
  53. package/src/tool/book-royalty-break-even-calculator/i18n/sv.ts +2 -0
  54. package/src/tool/book-royalty-break-even-calculator/i18n/tr.ts +2 -0
  55. package/src/tool/book-royalty-break-even-calculator/i18n/zh.ts +2 -0
  56. package/src/tool/book-royalty-break-even-calculator/index.ts +11 -0
  57. package/src/tool/book-royalty-break-even-calculator/logic.test.ts +39 -0
  58. package/src/tool/book-royalty-break-even-calculator/logic.ts +91 -0
  59. package/src/tool/book-royalty-break-even-calculator/seo.astro +12 -0
  60. package/src/tool/book-royalty-break-even-calculator/ui.ts +46 -0
  61. package/src/tools.ts +3 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jjlmoya/utils-books",
3
- "version": "1.13.0",
3
+ "version": "1.15.0",
4
4
  "type": "module",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -1,11 +1,13 @@
1
1
  import { bookPaginationAndSpineCalculator } from '../tool/book-pagination-and-spine-calculator/entry';
2
2
  import { bookInteriorMarginAndGutterPlanner } from '../tool/book-interior-margin-and-gutter-planner/entry';
3
3
  import { bookIndexPageBudgetCalculator } from '../tool/book-index-page-budget-calculator/entry';
4
+ import { bookRoyaltyBreakEvenCalculator } from '../tool/book-royalty-break-even-calculator/entry';
5
+ import { bookPrintSignatureCalculator } from '../tool/book-print-signature-calculator/entry';
4
6
  import type { CategoryLocaleContent, KnownLocale } from '../types';
5
7
 
6
8
  export const booksCategory = {
7
9
  icon: 'mdi:book-open-page-variant-outline',
8
- tools: [bookPaginationAndSpineCalculator, bookInteriorMarginAndGutterPlanner, bookIndexPageBudgetCalculator],
10
+ tools: [bookPaginationAndSpineCalculator, bookInteriorMarginAndGutterPlanner, bookIndexPageBudgetCalculator, bookRoyaltyBreakEvenCalculator, bookPrintSignatureCalculator],
9
11
  i18n: {
10
12
  de: () => import('./i18n/de').then((module) => module.content),
11
13
  en: () => import('./i18n/en').then((module) => module.content),
package/src/entries.ts CHANGED
@@ -4,10 +4,14 @@ export { bookReadingTimeDeadlinePlanner } from './tool/book-reading-time-deadlin
4
4
  export type { BookReadingUI, BookReadingLocaleContent } from './tool/book-reading-time-deadline-planner/entry';
5
5
  export { bookIndexPageBudgetCalculator } from './tool/book-index-page-budget-calculator/entry';
6
6
  export type { BookIndexUI, BookIndexLocaleContent } from './tool/book-index-page-budget-calculator/entry';
7
+ export { bookRoyaltyBreakEvenCalculator } from './tool/book-royalty-break-even-calculator/entry';
8
+ export type { BookRoyaltyUI, BookRoyaltyLocaleContent } from './tool/book-royalty-break-even-calculator/entry';
7
9
 
8
10
  import { bookPaginationAndSpineCalculator } from './tool/book-pagination-and-spine-calculator/entry';
9
11
  import { bookInteriorMarginAndGutterPlanner } from './tool/book-interior-margin-and-gutter-planner/entry';
10
12
  import { bookReadingTimeDeadlinePlanner } from './tool/book-reading-time-deadline-planner/entry';
11
13
  import { bookIndexPageBudgetCalculator } from './tool/book-index-page-budget-calculator/entry';
14
+ import { bookRoyaltyBreakEvenCalculator } from './tool/book-royalty-break-even-calculator/entry';
15
+ import { bookPrintSignatureCalculator } from './tool/book-print-signature-calculator/entry';
12
16
 
13
- export const ALL_ENTRIES = [bookPaginationAndSpineCalculator, bookInteriorMarginAndGutterPlanner, bookReadingTimeDeadlinePlanner, bookIndexPageBudgetCalculator];
17
+ export const ALL_ENTRIES = [bookPaginationAndSpineCalculator, bookInteriorMarginAndGutterPlanner, bookReadingTimeDeadlinePlanner, bookIndexPageBudgetCalculator, bookRoyaltyBreakEvenCalculator, bookPrintSignatureCalculator];
@@ -18,6 +18,6 @@ describe('Locale Completeness Validation', () => {
18
18
  });
19
19
 
20
20
  it('all tools registered', () => {
21
- expect(ALL_TOOLS.length).toBe(4);
21
+ expect(ALL_TOOLS.length).toBe(6);
22
22
  });
23
23
  });
@@ -5,7 +5,7 @@ import { booksCategory } from '../data';
5
5
  describe('Tool Validation Suite', () => {
6
6
  describe('Library Registration', () => {
7
7
  it('should have tools in ALL_TOOLS', () => {
8
- expect(ALL_TOOLS.length).toBe(4);
8
+ expect(ALL_TOOLS.length).toBe(6);
9
9
  });
10
10
 
11
11
  it('booksCategory should be defined', () => {
@@ -0,0 +1,16 @@
1
+ ---
2
+ import { Bibliography as SharedBibliography } from '@jjlmoya/utils-shared';
3
+ import { bookPrintSignatureCalculator } 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 = bookPrintSignatureCalculator.i18n[locale] || bookPrintSignatureCalculator.i18n.en;
12
+ const content = await loader?.();
13
+ if (!content) return null;
14
+ ---
15
+
16
+ <SharedBibliography links={content.bibliography} />
@@ -0,0 +1,7 @@
1
+ import type { BibliographyEntry } from '../../types';
2
+
3
+ export const bibliography: BibliographyEntry[] = [
4
+ { name: 'ISO 216:2007. Writing paper and certain classes of printed matter', url: 'https://www.iso.org/standard/36631.html' },
5
+ { name: 'Library of Congress. Preserving Your Books', url: 'https://guides.loc.gov/preserving-your-books' },
6
+ { name: 'Bibliothèque nationale de France. Les reliures de la Réserve des Livres rares', url: 'https://www.bnf.fr/fr/les-reliures-de-la-reserve-des-livres-rares' },
7
+ ];
@@ -0,0 +1,350 @@
1
+ :root {
2
+ --signature-ink: #17202a;
3
+ --signature-muted: #68737c;
4
+ --signature-surface: #fff;
5
+ --signature-soft: #f5f1e9;
6
+ --signature-line: #ddd5c7;
7
+ --signature-accent: #8b4b35;
8
+ --signature-accent-soft: #f0d9ce;
9
+ --signature-paper: #ead7b0;
10
+ --signature-paper-edge: #c4a875;
11
+ --signature-on-accent: #fff;
12
+ --signature-shadow: rgba(23, 32, 42, 0.1);
13
+ }
14
+
15
+ .theme-dark {
16
+ --signature-ink: #f5f1e9;
17
+ --signature-muted: #b7c0c6;
18
+ --signature-surface: #1d252c;
19
+ --signature-soft: #273139;
20
+ --signature-line: #46515a;
21
+ --signature-accent: #e39b7c;
22
+ --signature-accent-soft: #4d322c;
23
+ --signature-paper: #80694a;
24
+ --signature-paper-edge: #ae8d5e;
25
+ --signature-on-accent: #17202a;
26
+ --signature-shadow: rgba(0, 0, 0, 0.28);
27
+ }
28
+
29
+ .book-signature-tool {
30
+ background: var(--signature-surface);
31
+ border: 1px solid var(--signature-line);
32
+ border-radius: 1.25rem;
33
+ box-shadow: 0 1rem 2.5rem var(--signature-shadow);
34
+ color: var(--signature-ink);
35
+ display: grid;
36
+ gap: 1rem;
37
+ grid-template-columns: minmax(290px, 0.9fr) minmax(400px, 1.1fr);
38
+ padding: clamp(1.25rem, 3vw, 2.25rem);
39
+ }
40
+
41
+ .signature-editor,
42
+ .signature-output {
43
+ min-width: 0;
44
+ }
45
+
46
+ .signature-output {
47
+ border-left: 1px solid var(--signature-line);
48
+ padding-left: clamp(1rem, 3vw, 2rem);
49
+ }
50
+
51
+ .signature-heading,
52
+ .signature-output-heading {
53
+ margin-bottom: 1.4rem;
54
+ }
55
+
56
+ .signature-eyebrow {
57
+ color: var(--signature-accent);
58
+ font-size: 0.7rem;
59
+ font-weight: 900;
60
+ letter-spacing: 0.12em;
61
+ text-transform: uppercase;
62
+ }
63
+
64
+ .book-signature-tool h2 {
65
+ font-size: clamp(1.5rem, 3vw, 2.4rem);
66
+ letter-spacing: -0.05em;
67
+ line-height: 1;
68
+ margin: 0.45rem 0;
69
+ }
70
+
71
+ .book-signature-tool h3 {
72
+ font-size: 1rem;
73
+ margin: 0;
74
+ }
75
+
76
+ .book-signature-tool p {
77
+ color: var(--signature-muted);
78
+ line-height: 1.5;
79
+ margin: 0;
80
+ }
81
+
82
+ .book-signature-tool label,
83
+ .book-signature-tool legend {
84
+ display: block;
85
+ font-size: 0.78rem;
86
+ font-weight: 800;
87
+ margin: 0.85rem 0 0.4rem;
88
+ }
89
+
90
+ .book-signature-tool label small,
91
+ .book-signature-tool fieldset > small {
92
+ color: var(--signature-muted);
93
+ display: block;
94
+ font-size: 0.7rem;
95
+ font-weight: 500;
96
+ line-height: 1.4;
97
+ margin-top: 0.25rem;
98
+ }
99
+
100
+ .book-signature-tool input,
101
+ .book-signature-tool select {
102
+ background: var(--signature-soft);
103
+ border: 1px solid var(--signature-line);
104
+ border-radius: 0.6rem;
105
+ box-sizing: border-box;
106
+ color: var(--signature-ink);
107
+ font: inherit;
108
+ margin-top: 0.45rem;
109
+ padding: 0.7rem;
110
+ width: 100%;
111
+ }
112
+
113
+ .book-signature-tool input:focus-visible,
114
+ .book-signature-tool select:focus-visible,
115
+ .book-signature-tool button:focus-visible {
116
+ outline: 3px solid var(--signature-accent-soft);
117
+ outline-offset: 2px;
118
+ }
119
+
120
+ .book-signature-tool fieldset {
121
+ border: 0;
122
+ margin: 1.2rem 0 0;
123
+ padding: 0;
124
+ }
125
+
126
+ .signature-mode-buttons {
127
+ display: flex;
128
+ flex-wrap: wrap;
129
+ gap: 0.45rem;
130
+ }
131
+
132
+ .book-signature-tool button {
133
+ background: var(--signature-soft);
134
+ border: 1px solid var(--signature-line);
135
+ border-radius: 999px;
136
+ color: var(--signature-ink);
137
+ cursor: pointer;
138
+ font: inherit;
139
+ font-size: 0.75rem;
140
+ font-weight: 800;
141
+ padding: 0.55rem 0.75rem;
142
+ }
143
+
144
+ .book-signature-tool button:hover,
145
+ .book-signature-tool button.active {
146
+ border-color: var(--signature-accent);
147
+ }
148
+
149
+ .signature-primary,
150
+ .signature-live-badge {
151
+ background: var(--signature-accent);
152
+ border-color: var(--signature-accent);
153
+ color: var(--signature-on-accent);
154
+ }
155
+
156
+ .signature-actions {
157
+ margin-top: 1.25rem;
158
+ }
159
+
160
+ .blank-pages-fieldset {
161
+ background: var(--signature-soft);
162
+ border-radius: 0.8rem;
163
+ padding: 0.75rem 1rem;
164
+ }
165
+
166
+ .blank-pages-fieldset legend {
167
+ margin-top: 0;
168
+ }
169
+
170
+ .checkbox-label {
171
+ align-items: flex-start;
172
+ display: flex;
173
+ gap: 0.5rem;
174
+ margin: 0.55rem 0 0.2rem;
175
+ }
176
+
177
+ .checkbox-label input {
178
+ flex: 0 0 auto;
179
+ margin: 0.1rem 0 0;
180
+ width: 1.1rem;
181
+ }
182
+
183
+ .signature-output-heading {
184
+ align-items: flex-start;
185
+ display: flex;
186
+ justify-content: space-between;
187
+ }
188
+
189
+ .signature-live-badge {
190
+ border-radius: 999px;
191
+ font-size: 0.62rem;
192
+ font-weight: 900;
193
+ letter-spacing: 0.1em;
194
+ padding: 0.4rem 0.6rem;
195
+ }
196
+
197
+ .signature-results {
198
+ display: grid;
199
+ gap: 0.55rem;
200
+ grid-template-columns: repeat(2, 1fr);
201
+ }
202
+
203
+ .signature-result-card {
204
+ background: var(--signature-soft);
205
+ border-radius: 0.8rem;
206
+ padding: 0.8rem;
207
+ }
208
+
209
+ .signature-result-card:first-child {
210
+ background: var(--signature-accent-soft);
211
+ }
212
+
213
+ .signature-result-card span,
214
+ .signature-result-card small {
215
+ color: var(--signature-muted);
216
+ display: block;
217
+ font-size: 0.64rem;
218
+ font-weight: 800;
219
+ text-transform: uppercase;
220
+ }
221
+
222
+ .signature-result-card strong {
223
+ display: block;
224
+ font-size: clamp(1.2rem, 2.5vw, 1.8rem);
225
+ margin: 0.3rem 0;
226
+ }
227
+
228
+ .signature-map-panel {
229
+ background: var(--signature-soft);
230
+ border-radius: 1rem;
231
+ margin-top: 1rem;
232
+ padding: 1rem;
233
+ }
234
+
235
+ .signature-map-panel h3 {
236
+ margin-bottom: 0.7rem;
237
+ }
238
+
239
+ .signature-map {
240
+ display: grid;
241
+ gap: 0.45rem;
242
+ grid-template-columns: repeat(2, 1fr);
243
+ max-height: 16rem;
244
+ overflow: auto;
245
+ padding-right: 0.25rem;
246
+ }
247
+
248
+ .signature-map-item {
249
+ background: var(--signature-surface);
250
+ border-left: 0.3rem solid var(--signature-accent);
251
+ border-radius: 0.55rem;
252
+ display: grid;
253
+ gap: 0.1rem;
254
+ padding: 0.6rem 0.7rem;
255
+ }
256
+
257
+ .signature-map-item.has-blanks {
258
+ border-left-color: #c27832;
259
+ }
260
+
261
+ .signature-map-item span,
262
+ .signature-map-item small,
263
+ .signature-map-item em {
264
+ color: var(--signature-muted);
265
+ font-size: 0.68rem;
266
+ }
267
+
268
+ .signature-map-item strong {
269
+ font-size: 0.76rem;
270
+ }
271
+
272
+ .signature-map-item em {
273
+ color: var(--signature-accent);
274
+ font-style: normal;
275
+ font-weight: 800;
276
+ }
277
+
278
+ .signature-preview {
279
+ align-items: center;
280
+ background: var(--signature-soft);
281
+ border-radius: 1rem;
282
+ display: flex;
283
+ flex-direction: column;
284
+ justify-content: center;
285
+ margin-top: 1rem;
286
+ min-height: 13rem;
287
+ overflow: hidden;
288
+ padding: 1rem;
289
+ position: relative;
290
+ }
291
+
292
+ .signature-stack {
293
+ height: 9rem;
294
+ position: relative;
295
+ transform: rotate(-4deg);
296
+ width: min(18rem, 90%);
297
+ }
298
+
299
+ .signature-sheet {
300
+ background: var(--signature-paper);
301
+ border: 1px solid var(--signature-paper-edge);
302
+ box-shadow: 0.35rem 0.35rem 0.5rem var(--signature-shadow);
303
+ height: 6rem;
304
+ left: var(--sheet-offset);
305
+ position: absolute;
306
+ top: var(--sheet-offset);
307
+ transform: rotate(var(--sheet-tilt));
308
+ width: 13rem;
309
+ }
310
+
311
+ .signature-fold {
312
+ border-left: 1px dashed var(--signature-accent);
313
+ border-right: 1px dashed var(--signature-accent);
314
+ height: 100%;
315
+ left: 35%;
316
+ opacity: 0.7;
317
+ position: absolute;
318
+ width: 30%;
319
+ }
320
+
321
+ .signature-preview p,
322
+ .signature-source-note {
323
+ font-size: 0.72rem;
324
+ margin-top: 1rem;
325
+ text-align: center;
326
+ }
327
+
328
+ .signature-method-note {
329
+ border-left: 3px solid var(--signature-accent);
330
+ font-size: 0.78rem;
331
+ margin-top: 1rem;
332
+ padding-left: 0.75rem;
333
+ }
334
+
335
+ @media (max-width: 760px) {
336
+ .book-signature-tool {
337
+ grid-template-columns: 1fr;
338
+ }
339
+
340
+ .signature-output {
341
+ border-left: 0;
342
+ border-top: 1px solid var(--signature-line);
343
+ padding-left: 0;
344
+ padding-top: 1.5rem;
345
+ }
346
+
347
+ .signature-map {
348
+ grid-template-columns: 1fr;
349
+ }
350
+ }
@@ -0,0 +1,38 @@
1
+ ---
2
+ import './book-print-signature-calculator.css';
3
+ import type { BookPrintSignatureUI } from './ui';
4
+
5
+ interface Props {
6
+ ui: BookPrintSignatureUI;
7
+ }
8
+
9
+ const { ui } = Astro.props;
10
+ ---
11
+
12
+ <div class="book-signature-tool" data-ui={JSON.stringify(ui)}>
13
+ <div class="signature-editor">
14
+ <div class="signature-heading"><span class="signature-eyebrow">{ui.title}</span><h2>{ui.methodTitle}</h2><p>{ui.methodHint}</p></div>
15
+ <label for="interior-pages">{ui.interiorPagesLabel}<small>{ui.interiorPagesHint}</small><input id="interior-pages" type="number" min="1" max="10000" step="1" value="192" /></label>
16
+ <label for="pages-per-signature">{ui.pagesPerSignatureLabel}<small>{ui.pagesPerSignatureHint}</small><select id="pages-per-signature"><option value="8">8</option><option value="16" selected>16</option><option value="32">32</option></select></label>
17
+ <fieldset>
18
+ <legend>{ui.foldModeLabel}</legend>
19
+ <div class="signature-mode-buttons"><button type="button" data-fold-mode="saddle-stitch" aria-pressed="false">{ui.saddleStitchLabel}</button><button type="button" data-fold-mode="gathered" aria-pressed="false">{ui.gatheredLabel}</button><button type="button" data-fold-mode="perfect-bound" aria-pressed="false">{ui.perfectBoundLabel}</button></div>
20
+ </fieldset>
21
+ <fieldset class="blank-pages-fieldset">
22
+ <legend>{ui.blankPagesLabel}</legend>
23
+ <label class="checkbox-label" for="allow-blank-pages"><input id="allow-blank-pages" type="checkbox" checked /> <span data-blank-status>{ui.blankPagesAllowed}</span></label>
24
+ <small>{ui.blankPagesHint}</small>
25
+ </fieldset>
26
+ <div class="signature-actions"><button type="button" class="signature-primary" data-action="reset">{ui.resetLabel}</button></div>
27
+ </div>
28
+ <div class="signature-output">
29
+ <div class="signature-output-heading"><div><span class="signature-eyebrow">{ui.resultTitle}</span><h2>{ui.signaturesLabel}</h2></div><span class="signature-live-badge">LIVE</span></div>
30
+ <div id="signature-results" class="signature-results" aria-live="polite"></div>
31
+ <div class="signature-map-panel"><h3>{ui.signatureMapLabel}</h3><div id="signature-map" class="signature-map"></div></div>
32
+ <div id="signature-preview" class="signature-preview"></div>
33
+ <p class="signature-method-note">{ui.methodNote}</p>
34
+ <p class="signature-source-note">{ui.sourceLabel}</p>
35
+ </div>
36
+ </div>
37
+
38
+ <script src="./controller.ts"></script>
@@ -0,0 +1,71 @@
1
+ import { calculateSignatures, STANDARD_PRESET, type FoldMode, type SignatureInputs } from './logic';
2
+ import { renderMap, renderPreview, renderResults } from './dom-views';
3
+ import type { BookPrintSignatureUI } from './ui';
4
+
5
+ const root = document.querySelector<HTMLElement>('.book-signature-tool');
6
+ if (!root) throw new Error('Book print signature tool root not found');
7
+
8
+ const ui = JSON.parse(root.dataset.ui ?? '{}') as BookPrintSignatureUI;
9
+ const interiorPages = root.querySelector<HTMLInputElement>('#interior-pages');
10
+ const pagesPerSignature = root.querySelector<HTMLSelectElement>('#pages-per-signature');
11
+ const allowBlankPages = root.querySelector<HTMLInputElement>('#allow-blank-pages');
12
+ const blankStatus = root.querySelector<HTMLElement>('[data-blank-status]');
13
+ const result = root.querySelector<HTMLElement>('#signature-results');
14
+ const map = root.querySelector<HTMLElement>('#signature-map');
15
+ const preview = root.querySelector<HTMLElement>('#signature-preview');
16
+ const modeButtons = [...root.querySelectorAll<HTMLButtonElement>('[data-fold-mode]')];
17
+
18
+ if (!interiorPages || !pagesPerSignature || !allowBlankPages || !result || !map || !preview) {
19
+ throw new Error('Book print signature tool controls are incomplete');
20
+ }
21
+
22
+ const interiorPagesInput = interiorPages;
23
+ const pagesPerSignatureSelect = pagesPerSignature;
24
+ const allowBlankPagesInput = allowBlankPages;
25
+ const resultContainer = result;
26
+ const mapContainer = map;
27
+ const previewContainer = preview;
28
+
29
+ let foldMode: FoldMode = STANDARD_PRESET.foldMode;
30
+
31
+ function readInputs(): SignatureInputs {
32
+ return {
33
+ interiorPages: Number(interiorPagesInput.value),
34
+ pagesPerSignature: Number(pagesPerSignatureSelect.value),
35
+ foldMode,
36
+ allowBlankPages: allowBlankPagesInput.checked,
37
+ };
38
+ }
39
+
40
+ function render(): void {
41
+ const calculation = calculateSignatures(readInputs());
42
+ renderResults(resultContainer, calculation, ui);
43
+ renderMap(mapContainer, calculation, ui);
44
+ renderPreview(previewContainer, calculation, ui);
45
+ modeButtons.forEach((button) => {
46
+ const isActive = button.dataset.foldMode === foldMode;
47
+ button.classList.toggle('active', isActive);
48
+ button.setAttribute('aria-pressed', String(isActive));
49
+ });
50
+ if (blankStatus) blankStatus.textContent = allowBlankPagesInput.checked ? ui.blankPagesAllowed : ui.blankPagesNotAllowed;
51
+ }
52
+
53
+ function reset(): void {
54
+ interiorPagesInput.value = String(STANDARD_PRESET.interiorPages);
55
+ pagesPerSignatureSelect.value = String(STANDARD_PRESET.pagesPerSignature);
56
+ allowBlankPagesInput.checked = STANDARD_PRESET.allowBlankPages;
57
+ foldMode = STANDARD_PRESET.foldMode;
58
+ render();
59
+ }
60
+
61
+ interiorPagesInput.addEventListener('input', render);
62
+ pagesPerSignatureSelect.addEventListener('change', render);
63
+ allowBlankPagesInput.addEventListener('change', render);
64
+ modeButtons.forEach((button) => button.addEventListener('click', () => {
65
+ const mode = button.dataset.foldMode;
66
+ if (mode === 'saddle-stitch' || mode === 'gathered' || mode === 'perfect-bound') foldMode = mode;
67
+ render();
68
+ }));
69
+ root.querySelector<HTMLButtonElement>('[data-action="reset"]')?.addEventListener('click', reset);
70
+
71
+ render();
@@ -0,0 +1,70 @@
1
+ import type { BookPrintSignatureUI } from './ui';
2
+ import type { SignatureCalculation } from './logic';
3
+
4
+ export function renderResults(container: HTMLElement, calculation: SignatureCalculation, ui: BookPrintSignatureUI): void {
5
+ container.replaceChildren(
6
+ resultCard(ui.signaturesLabel, formatNumber(calculation.signatureCount), ui.pagesPerSignatureHint.replace('{pages}', formatNumber(calculation.pagesPerSignature))),
7
+ resultCard(ui.plannedPagesLabel, formatNumber(calculation.plannedPages), ui.pagesLabel),
8
+ resultCard(ui.blankCountLabel, formatNumber(calculation.blankPages), ui.blankLabel),
9
+ resultCard(ui.remainderLabel, formatNumber(calculation.remainderPages), calculation.hasIncompleteSignature && calculation.blankPages === 0 ? ui.reviewLabel : ui.completeLabel),
10
+ );
11
+ }
12
+
13
+ export function renderMap(container: HTMLElement, calculation: SignatureCalculation, ui: BookPrintSignatureUI): void {
14
+ const fragment = document.createDocumentFragment();
15
+ calculation.ranges.forEach((range) => {
16
+ const item = document.createElement('div');
17
+ item.className = `signature-map-item${range.blankPages > 0 ? ' has-blanks' : ''}`;
18
+ const heading = document.createElement('strong');
19
+ heading.textContent = `${ui.signatureLabel} ${formatNumber(range.number)}`;
20
+ const rangeText = document.createElement('span');
21
+ rangeText.textContent = `${ui.pageRangeLabel}: ${formatNumber(range.startPage)}-${formatNumber(range.endPage)}`;
22
+ const pageText = document.createElement('small');
23
+ pageText.textContent = `${formatNumber(calculation.pagesPerSignature)} ${ui.pagesLabel}`;
24
+ item.append(heading, rangeText, pageText);
25
+ if (range.blankPages > 0) {
26
+ const blankText = document.createElement('em');
27
+ blankText.textContent = `+ ${formatNumber(range.blankPages)} ${ui.blankLabel}`;
28
+ item.append(blankText);
29
+ }
30
+ fragment.append(item);
31
+ });
32
+ container.replaceChildren(fragment);
33
+ }
34
+
35
+ export function renderPreview(container: HTMLElement, calculation: SignatureCalculation, ui: BookPrintSignatureUI): void {
36
+ const stack = document.createElement('div');
37
+ stack.className = 'signature-stack';
38
+ const visibleSheets = Math.min(6, Math.max(2, Math.ceil(calculation.signatureCount / 2)));
39
+ for (let index = visibleSheets - 1; index >= 0; index -= 1) {
40
+ const sheet = document.createElement('div');
41
+ sheet.className = 'signature-sheet';
42
+ sheet.style.setProperty('--sheet-offset', `${index * 0.35}rem`);
43
+ sheet.style.setProperty('--sheet-tilt', `${(index % 2 === 0 ? -1 : 1) * (index + 1)}deg`);
44
+ const fold = document.createElement('span');
45
+ fold.className = 'signature-fold';
46
+ fold.setAttribute('aria-hidden', 'true');
47
+ sheet.append(fold);
48
+ stack.append(sheet);
49
+ }
50
+ const caption = document.createElement('p');
51
+ caption.textContent = `${ui.previewLabel}: ${ui.previewHint} ${formatNumber(calculation.pagesPerSignature)} ${ui.pagesLabel}.`;
52
+ container.replaceChildren(stack, caption);
53
+ }
54
+
55
+ function resultCard(label: string, value: string, note: string): HTMLElement {
56
+ const card = document.createElement('div');
57
+ card.className = 'signature-result-card';
58
+ const labelElement = document.createElement('span');
59
+ labelElement.textContent = label;
60
+ const valueElement = document.createElement('strong');
61
+ valueElement.textContent = value;
62
+ const noteElement = document.createElement('small');
63
+ noteElement.textContent = note;
64
+ card.append(labelElement, valueElement, noteElement);
65
+ return card;
66
+ }
67
+
68
+ function formatNumber(value: number): string {
69
+ return new Intl.NumberFormat('en-US', { maximumFractionDigits: 0 }).format(value);
70
+ }
@@ -0,0 +1,27 @@
1
+ import type { BooksToolEntry, ToolLocaleContent } from '../../types';
2
+ import type { BookPrintSignatureUI } from './ui';
3
+
4
+ export type { BookPrintSignatureUI } from './ui';
5
+ export type BookPrintSignatureLocaleContent = ToolLocaleContent<BookPrintSignatureUI>;
6
+
7
+ export const bookPrintSignatureCalculator: BooksToolEntry<BookPrintSignatureUI> = {
8
+ id: 'book-print-signature-calculator',
9
+ icons: { bg: 'mdi:book-open-outline', fg: 'mdi:book-open-page-variant-outline' },
10
+ i18n: {
11
+ de: () => import('./i18n/de').then((module) => module.content),
12
+ en: () => import('./i18n/en').then((module) => module.content),
13
+ es: () => import('./i18n/es').then((module) => module.content),
14
+ fr: () => import('./i18n/fr').then((module) => module.content),
15
+ id: () => import('./i18n/id').then((module) => module.content),
16
+ it: () => import('./i18n/it').then((module) => module.content),
17
+ ja: () => import('./i18n/ja').then((module) => module.content),
18
+ ko: () => import('./i18n/ko').then((module) => module.content),
19
+ nl: () => import('./i18n/nl').then((module) => module.content),
20
+ pl: () => import('./i18n/pl').then((module) => module.content),
21
+ pt: () => import('./i18n/pt').then((module) => module.content),
22
+ ru: () => import('./i18n/ru').then((module) => module.content),
23
+ sv: () => import('./i18n/sv').then((module) => module.content),
24
+ tr: () => import('./i18n/tr').then((module) => module.content),
25
+ zh: () => import('./i18n/zh').then((module) => module.content),
26
+ },
27
+ };