@jjlmoya/utils-books 1.14.0 → 1.16.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 (34) hide show
  1. package/package.json +2 -2
  2. package/scripts/validate-icons.mjs +57 -0
  3. package/src/category/index.ts +2 -1
  4. package/src/entries.ts +2 -1
  5. package/src/tests/locale_completeness.test.ts +1 -1
  6. package/src/tests/tool_validation.test.ts +1 -1
  7. package/src/tool/book-print-signature-calculator/bibliography.astro +16 -0
  8. package/src/tool/book-print-signature-calculator/bibliography.ts +7 -0
  9. package/src/tool/book-print-signature-calculator/book-print-signature-calculator.css +350 -0
  10. package/src/tool/book-print-signature-calculator/component.astro +38 -0
  11. package/src/tool/book-print-signature-calculator/controller.ts +71 -0
  12. package/src/tool/book-print-signature-calculator/dom-views.ts +70 -0
  13. package/src/tool/book-print-signature-calculator/entry.ts +27 -0
  14. package/src/tool/book-print-signature-calculator/i18n/de.ts +53 -0
  15. package/src/tool/book-print-signature-calculator/i18n/en.ts +85 -0
  16. package/src/tool/book-print-signature-calculator/i18n/es.ts +53 -0
  17. package/src/tool/book-print-signature-calculator/i18n/fr.ts +53 -0
  18. package/src/tool/book-print-signature-calculator/i18n/id.ts +53 -0
  19. package/src/tool/book-print-signature-calculator/i18n/it.ts +53 -0
  20. package/src/tool/book-print-signature-calculator/i18n/ja.ts +53 -0
  21. package/src/tool/book-print-signature-calculator/i18n/ko.ts +53 -0
  22. package/src/tool/book-print-signature-calculator/i18n/nl.ts +53 -0
  23. package/src/tool/book-print-signature-calculator/i18n/pl.ts +53 -0
  24. package/src/tool/book-print-signature-calculator/i18n/pt.ts +53 -0
  25. package/src/tool/book-print-signature-calculator/i18n/ru.ts +53 -0
  26. package/src/tool/book-print-signature-calculator/i18n/sv.ts +53 -0
  27. package/src/tool/book-print-signature-calculator/i18n/tr.ts +53 -0
  28. package/src/tool/book-print-signature-calculator/i18n/zh.ts +53 -0
  29. package/src/tool/book-print-signature-calculator/index.ts +11 -0
  30. package/src/tool/book-print-signature-calculator/logic.test.ts +49 -0
  31. package/src/tool/book-print-signature-calculator/logic.ts +88 -0
  32. package/src/tool/book-print-signature-calculator/seo.astro +16 -0
  33. package/src/tool/book-print-signature-calculator/ui.ts +35 -0
  34. package/src/tools.ts +2 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jjlmoya/utils-books",
3
- "version": "1.14.0",
3
+ "version": "1.16.0",
4
4
  "type": "module",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -46,7 +46,7 @@
46
46
  "postinstall": "node scripts/postinstall.mjs",
47
47
  "predev": "node scripts/postinstall.mjs",
48
48
  "prestart": "node scripts/postinstall.mjs",
49
- "prebuild": "node scripts/postinstall.mjs",
49
+ "prebuild": "node scripts/postinstall.mjs && node scripts/validate-icons.mjs",
50
50
  "qa": "npm run lint && npm run test && npm run build",
51
51
  "cf:dry-run": "npm run build && wrangler deploy --dry-run",
52
52
  "cf:preview": "npm run build && wrangler deploy --config wrangler.staging.jsonc",
@@ -0,0 +1,57 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { createRequire } from 'node:module';
4
+ import { readFileSync, readdirSync } from 'node:fs';
5
+ import { dirname, join, relative, resolve } from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
7
+
8
+ const require = createRequire(import.meta.url);
9
+ const packageJsonPath = require.resolve('@iconify-json/mdi/package.json');
10
+ const packageRoot = dirname(packageJsonPath);
11
+ const iconSet = JSON.parse(readFileSync(join(packageRoot, 'icons.json'), 'utf8'));
12
+ const availableIcons = new Set([
13
+ ...Object.keys(iconSet.icons ?? {}),
14
+ ...Object.keys(iconSet.aliases ?? {}),
15
+ ]);
16
+
17
+ const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
18
+ const sourceRoot = resolve(repoRoot, 'src');
19
+ const extensions = new Set(['.astro', '.js', '.mjs', '.ts', '.tsx']);
20
+ const iconPattern = /\bmdi:([a-z0-9-]+)\b/g;
21
+ const failures = [];
22
+ let references = 0;
23
+
24
+ function walk(directory) {
25
+ const entries = readdirSync(directory, { withFileTypes: true });
26
+ for (const entry of entries) {
27
+ const path = join(directory, entry.name);
28
+ if (entry.isDirectory()) {
29
+ if (entry.name !== 'node_modules' && entry.name !== 'dist' && entry.name !== 'tests') {
30
+ walk(path);
31
+ }
32
+ continue;
33
+ }
34
+ if (!extensions.has(path.slice(path.lastIndexOf('.')))) continue;
35
+
36
+ const source = readFileSync(path, 'utf8');
37
+ for (const match of source.matchAll(iconPattern)) {
38
+ references += 1;
39
+ const iconName = match[1];
40
+ if (availableIcons.has(iconName)) continue;
41
+
42
+ const line = source.slice(0, match.index).split('\n').length;
43
+ failures.push(`${relative(repoRoot, path)}:${line} — mdi:${iconName}`);
44
+ }
45
+ }
46
+ }
47
+
48
+ walk(sourceRoot);
49
+
50
+ if (failures.length > 0) {
51
+ console.error('Invalid MDI icons found:');
52
+ for (const failure of failures) console.error(`- ${failure}`);
53
+ console.error(`Checked ${references} MDI icon references against @iconify-json/mdi.`);
54
+ process.exitCode = 1;
55
+ } else {
56
+ console.log(`MDI icon validation passed: ${references} references checked.`);
57
+ }
@@ -2,11 +2,12 @@ import { bookPaginationAndSpineCalculator } from '../tool/book-pagination-and-sp
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
4
  import { bookRoyaltyBreakEvenCalculator } from '../tool/book-royalty-break-even-calculator/entry';
5
+ import { bookPrintSignatureCalculator } from '../tool/book-print-signature-calculator/entry';
5
6
  import type { CategoryLocaleContent, KnownLocale } from '../types';
6
7
 
7
8
  export const booksCategory = {
8
9
  icon: 'mdi:book-open-page-variant-outline',
9
- tools: [bookPaginationAndSpineCalculator, bookInteriorMarginAndGutterPlanner, bookIndexPageBudgetCalculator, bookRoyaltyBreakEvenCalculator],
10
+ tools: [bookPaginationAndSpineCalculator, bookInteriorMarginAndGutterPlanner, bookIndexPageBudgetCalculator, bookRoyaltyBreakEvenCalculator, bookPrintSignatureCalculator],
10
11
  i18n: {
11
12
  de: () => import('./i18n/de').then((module) => module.content),
12
13
  en: () => import('./i18n/en').then((module) => module.content),
package/src/entries.ts CHANGED
@@ -12,5 +12,6 @@ import { bookInteriorMarginAndGutterPlanner } from './tool/book-interior-margin-
12
12
  import { bookReadingTimeDeadlinePlanner } from './tool/book-reading-time-deadline-planner/entry';
13
13
  import { bookIndexPageBudgetCalculator } from './tool/book-index-page-budget-calculator/entry';
14
14
  import { bookRoyaltyBreakEvenCalculator } from './tool/book-royalty-break-even-calculator/entry';
15
+ import { bookPrintSignatureCalculator } from './tool/book-print-signature-calculator/entry';
15
16
 
16
- export const ALL_ENTRIES = [bookPaginationAndSpineCalculator, bookInteriorMarginAndGutterPlanner, bookReadingTimeDeadlinePlanner, bookIndexPageBudgetCalculator, bookRoyaltyBreakEvenCalculator];
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(5);
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(5);
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
+ }