@jjlmoya/utils-books 1.6.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.
Files changed (36) hide show
  1. package/package.json +1 -1
  2. package/src/category/index.ts +2 -1
  3. package/src/entries.ts +2 -1
  4. package/src/index.ts +1 -0
  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-interior-margin-and-gutter-planner/bibliography.astro +16 -0
  8. package/src/tool/book-interior-margin-and-gutter-planner/bibliography.ts +12 -0
  9. package/src/tool/book-interior-margin-and-gutter-planner/book-interior-margin-and-gutter-planner.css +462 -0
  10. package/src/tool/book-interior-margin-and-gutter-planner/component.astro +102 -0
  11. package/src/tool/book-interior-margin-and-gutter-planner/controller.ts +141 -0
  12. package/src/tool/book-interior-margin-and-gutter-planner/dom-views.ts +66 -0
  13. package/src/tool/book-interior-margin-and-gutter-planner/entry.ts +27 -0
  14. package/src/tool/book-interior-margin-and-gutter-planner/evaluator.ts +22 -0
  15. package/src/tool/book-interior-margin-and-gutter-planner/i18n/de.ts +59 -0
  16. package/src/tool/book-interior-margin-and-gutter-planner/i18n/en.ts +60 -0
  17. package/src/tool/book-interior-margin-and-gutter-planner/i18n/es.ts +59 -0
  18. package/src/tool/book-interior-margin-and-gutter-planner/i18n/fr.ts +59 -0
  19. package/src/tool/book-interior-margin-and-gutter-planner/i18n/id.ts +59 -0
  20. package/src/tool/book-interior-margin-and-gutter-planner/i18n/it.ts +59 -0
  21. package/src/tool/book-interior-margin-and-gutter-planner/i18n/ja.ts +58 -0
  22. package/src/tool/book-interior-margin-and-gutter-planner/i18n/ko.ts +58 -0
  23. package/src/tool/book-interior-margin-and-gutter-planner/i18n/nl.ts +59 -0
  24. package/src/tool/book-interior-margin-and-gutter-planner/i18n/pl.ts +59 -0
  25. package/src/tool/book-interior-margin-and-gutter-planner/i18n/pt.ts +59 -0
  26. package/src/tool/book-interior-margin-and-gutter-planner/i18n/ru.ts +58 -0
  27. package/src/tool/book-interior-margin-and-gutter-planner/i18n/sv.ts +59 -0
  28. package/src/tool/book-interior-margin-and-gutter-planner/i18n/tr.ts +58 -0
  29. package/src/tool/book-interior-margin-and-gutter-planner/i18n/zh.ts +58 -0
  30. package/src/tool/book-interior-margin-and-gutter-planner/index.ts +11 -0
  31. package/src/tool/book-interior-margin-and-gutter-planner/logic.test.ts +43 -0
  32. package/src/tool/book-interior-margin-and-gutter-planner/logic.ts +85 -0
  33. package/src/tool/book-interior-margin-and-gutter-planner/seo.astro +15 -0
  34. package/src/tool/book-interior-margin-and-gutter-planner/storage.ts +25 -0
  35. package/src/tool/book-interior-margin-and-gutter-planner/ui.ts +42 -0
  36. 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.6.0",
3
+ "version": "1.7.0",
4
4
  "type": "module",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -1,9 +1,10 @@
1
1
  import { bookPaginationAndSpineCalculator } from '../tool/book-pagination-and-spine-calculator/entry';
2
+ import { bookInteriorMarginAndGutterPlanner } from '../tool/book-interior-margin-and-gutter-planner/entry';
2
3
  import type { CategoryLocaleContent, KnownLocale } from '../types';
3
4
 
4
5
  export const booksCategory = {
5
6
  icon: 'mdi:book-open-page-variant-outline',
6
- tools: [bookPaginationAndSpineCalculator],
7
+ tools: [bookPaginationAndSpineCalculator, bookInteriorMarginAndGutterPlanner],
7
8
  i18n: {
8
9
  de: () => import('./i18n/de').then((module) => module.content),
9
10
  en: () => import('./i18n/en').then((module) => module.content),
package/src/entries.ts CHANGED
@@ -2,5 +2,6 @@ export { bookPaginationAndSpineCalculator } from './tool/book-pagination-and-spi
2
2
  export type { BookPaginationUI, BookPaginationLocaleContent } from './tool/book-pagination-and-spine-calculator/entry';
3
3
 
4
4
  import { bookPaginationAndSpineCalculator } from './tool/book-pagination-and-spine-calculator/entry';
5
+ import { bookInteriorMarginAndGutterPlanner } from './tool/book-interior-margin-and-gutter-planner/entry';
5
6
 
6
- export const ALL_ENTRIES = [bookPaginationAndSpineCalculator];
7
+ export const ALL_ENTRIES = [bookPaginationAndSpineCalculator, bookInteriorMarginAndGutterPlanner];
package/src/index.ts CHANGED
@@ -2,6 +2,7 @@ export { booksCategory } from './category';
2
2
  export const booksCategorySEO = () => import('./category/BooksCategorySEO.astro').then((module) => module.default);
3
3
 
4
4
  export { bookPaginationAndSpineCalculator, BOOK_PAGINATION_AND_SPINE_CALCULATOR_TOOL } from './tool/book-pagination-and-spine-calculator';
5
+ export { bookInteriorMarginAndGutterPlanner, BOOK_INTERIOR_MARGIN_AND_GUTTER_PLANNER_TOOL } from './tool/book-interior-margin-and-gutter-planner';
5
6
 
6
7
  export type {
7
8
  KnownLocale,
@@ -18,6 +18,6 @@ describe('Locale Completeness Validation', () => {
18
18
  });
19
19
 
20
20
  it('all tools registered', () => {
21
- expect(ALL_TOOLS.length).toBe(1);
21
+ expect(ALL_TOOLS.length).toBe(2);
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(1);
8
+ expect(ALL_TOOLS.length).toBe(2);
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 { bookInteriorMarginAndGutterPlanner } from './index';
4
+ import { bibliography } from './bibliography';
5
+ import type { KnownLocale } from '../../types';
6
+
7
+ interface Props {
8
+ locale?: KnownLocale;
9
+ }
10
+
11
+ const { locale = 'en' } = Astro.props as Props;
12
+ const loader = bookInteriorMarginAndGutterPlanner.i18n[locale] || bookInteriorMarginAndGutterPlanner.i18n.en;
13
+ const content = await loader?.();
14
+ ---
15
+
16
+ <SharedBibliography links={content?.bibliography || bibliography} />
@@ -0,0 +1,12 @@
1
+ import type { BibliographyEntry } from '../../types';
2
+
3
+ export const bibliography: BibliographyEntry[] = [
4
+ {
5
+ name: 'Amazon KDP. Set Trim Size, Bleed, and Margins',
6
+ url: 'https://kdp.amazon.com/en_US/help/topic/GVBQ3CMEQW3W2VL6',
7
+ },
8
+ {
9
+ name: 'Lulu. Preguntas frecuentes sobre el formato interior',
10
+ url: 'https://help.lulu.com/es/support/solutions/articles/64000255590-formato-interior-conceptos-b%C3%A1sicos',
11
+ },
12
+ ];
@@ -0,0 +1,462 @@
1
+ .n-tool {
2
+ --n-ink: #25221e;
3
+ --n-muted: #756f66;
4
+ --n-paper: #fffdf8;
5
+ --n-paper-alt: #f5efe3;
6
+ --n-line: #d7cbbb;
7
+ --n-accent: #a34e2b;
8
+ --n-accent-soft: #f4dfd1;
9
+ --n-ok: #37684d;
10
+ --n-warn: #a15b2e;
11
+ --n-bad: #9c3831;
12
+ --n-shadow: rgba(57, 39, 25, 0.12);
13
+
14
+ display: grid;
15
+ grid-template-columns: minmax(280px, 0.82fr) minmax(420px, 1.18fr);
16
+ min-height: 530px;
17
+ overflow: visible;
18
+ color: var(--n-ink);
19
+ background: var(--n-paper);
20
+ border: 1px solid var(--n-line);
21
+ box-shadow: 0 18px 40px var(--n-shadow);
22
+ }
23
+
24
+ .n-specs,
25
+ .n-result {
26
+ padding: 30px;
27
+ }
28
+
29
+ .n-specs {
30
+ display: flex;
31
+ flex-direction: column;
32
+ gap: 26px;
33
+ background: var(--n-paper-alt);
34
+ border-right: 1px solid var(--n-line);
35
+ }
36
+
37
+ .n-specs-head,
38
+ .n-scene-label,
39
+ .n-output-row {
40
+ display: flex;
41
+ align-items: center;
42
+ justify-content: space-between;
43
+ gap: 16px;
44
+ }
45
+
46
+ .n-eyebrow,
47
+ .n-scene-label,
48
+ .n-page-band {
49
+ color: var(--n-muted);
50
+ font-size: 0.7rem;
51
+ font-weight: 700;
52
+ letter-spacing: 0.12em;
53
+ text-transform: uppercase;
54
+ }
55
+
56
+ .n-unit-toggle {
57
+ display: inline-flex;
58
+ padding: 3px;
59
+ border: 1px solid var(--n-line);
60
+ border-radius: 999px;
61
+ background: var(--n-paper);
62
+ }
63
+
64
+ .n-unit-toggle button,
65
+ .n-select-trigger,
66
+ .n-select-menu button,
67
+ .n-bleed-toggle {
68
+ color: var(--n-ink);
69
+ font: inherit;
70
+ }
71
+
72
+ .n-unit-toggle button {
73
+ padding: 7px 10px;
74
+ border: 0;
75
+ border-radius: 999px;
76
+ background: transparent;
77
+ color: var(--n-muted);
78
+ cursor: pointer;
79
+ font-size: 0.7rem;
80
+ font-weight: 700;
81
+ }
82
+
83
+ .n-unit-toggle button[aria-pressed="true"] {
84
+ background: var(--n-ink);
85
+ color: var(--n-paper);
86
+ }
87
+
88
+ .n-field-grid,
89
+ .n-select-grid {
90
+ display: grid;
91
+ grid-template-columns: repeat(2, minmax(0, 1fr));
92
+ gap: 16px;
93
+ }
94
+
95
+ .n-field-grid .n-field-pages {
96
+ grid-column: 1 / -1;
97
+ }
98
+
99
+ .n-field {
100
+ position: relative;
101
+ display: flex;
102
+ min-width: 0;
103
+ flex-direction: column;
104
+ gap: 8px;
105
+ color: var(--n-muted);
106
+ font-size: 0.76rem;
107
+ font-weight: 700;
108
+ }
109
+
110
+ .n-field input,
111
+ .n-select-trigger {
112
+ width: 100%;
113
+ min-height: 46px;
114
+ padding: 10px 12px;
115
+ border: 1px solid var(--n-line);
116
+ border-radius: 2px;
117
+ background: var(--n-paper);
118
+ color: var(--n-ink);
119
+ font-size: 1rem;
120
+ font-weight: 600;
121
+ }
122
+
123
+ .n-field input:focus-visible,
124
+ .n-select-trigger:focus-visible,
125
+ .n-unit-toggle button:focus-visible,
126
+ .n-bleed-toggle:focus-visible,
127
+ .n-select-menu button:focus-visible {
128
+ outline: 3px solid var(--n-accent-soft);
129
+ outline-offset: 2px;
130
+ }
131
+
132
+ .n-field small {
133
+ color: var(--n-accent);
134
+ font-size: 0.7rem;
135
+ }
136
+
137
+ .n-select {
138
+ z-index: 3;
139
+ }
140
+
141
+ .n-select-trigger {
142
+ position: relative;
143
+ padding-right: 32px;
144
+ cursor: pointer;
145
+ text-align: left;
146
+ }
147
+
148
+ .n-select-trigger::after {
149
+ position: absolute;
150
+ top: 50%;
151
+ right: 12px;
152
+ width: 7px;
153
+ height: 7px;
154
+ border-right: 1.5px solid currentcolor;
155
+ border-bottom: 1.5px solid currentcolor;
156
+ content: '';
157
+ transform: translateY(-70%) rotate(45deg);
158
+ }
159
+
160
+ .n-select[data-open="true"] .n-select-trigger::after {
161
+ transform: translateY(-20%) rotate(225deg);
162
+ }
163
+
164
+ .n-select-menu {
165
+ position: absolute;
166
+ z-index: 5;
167
+ top: calc(100% + 6px);
168
+ right: 0;
169
+ left: 0;
170
+ padding: 5px;
171
+ border: 1px solid var(--n-line);
172
+ background: var(--n-paper);
173
+ box-shadow: 0 12px 25px var(--n-shadow);
174
+ }
175
+
176
+ .n-select-menu[hidden] {
177
+ display: none;
178
+ }
179
+
180
+ .n-select-menu button {
181
+ display: block;
182
+ width: 100%;
183
+ padding: 10px;
184
+ border: 0;
185
+ background: transparent;
186
+ cursor: pointer;
187
+ text-align: left;
188
+ }
189
+
190
+ .n-select-menu button:hover {
191
+ background: var(--n-accent-soft);
192
+ }
193
+
194
+ .n-bleed-toggle {
195
+ display: grid;
196
+ grid-template-columns: 18px 1fr auto;
197
+ align-items: center;
198
+ gap: 10px;
199
+ margin-top: auto;
200
+ padding: 14px 0 0;
201
+ border: 0;
202
+ border-top: 1px solid var(--n-line);
203
+ background: transparent;
204
+ cursor: pointer;
205
+ text-align: left;
206
+ }
207
+
208
+ .n-bleed-mark {
209
+ width: 18px;
210
+ height: 18px;
211
+ border: 1px solid var(--n-line);
212
+ border-radius: 50%;
213
+ background: var(--n-paper);
214
+ }
215
+
216
+ .n-bleed-toggle[aria-checked="true"] .n-bleed-mark {
217
+ border-color: var(--n-accent);
218
+ background: var(--n-accent);
219
+ box-shadow: inset 0 0 0 4px var(--n-paper-alt);
220
+ }
221
+
222
+ .n-bleed-toggle strong {
223
+ color: var(--n-accent);
224
+ font-size: 0.72rem;
225
+ text-transform: uppercase;
226
+ }
227
+
228
+ .n-result {
229
+ display: grid;
230
+ grid-template-rows: auto 1fr;
231
+ gap: 24px;
232
+ }
233
+
234
+ .n-result-copy {
235
+ display: grid;
236
+ grid-template-columns: minmax(190px, 0.85fr) minmax(190px, 1.15fr);
237
+ align-items: start;
238
+ gap: 26px;
239
+ }
240
+
241
+ .n-status {
242
+ display: flex;
243
+ align-items: flex-start;
244
+ gap: 11px;
245
+ min-height: 64px;
246
+ }
247
+
248
+ .n-status-dot {
249
+ flex: 0 0 auto;
250
+ width: 12px;
251
+ height: 12px;
252
+ margin-top: 5px;
253
+ border-radius: 50%;
254
+ background: var(--n-ok);
255
+ box-shadow: 0 0 0 5px rgba(55, 104, 77, 0.12);
256
+ }
257
+
258
+ [data-status="low-pages"] .n-status-dot,
259
+ [data-status="high-pages"] .n-status-dot {
260
+ background: var(--n-warn);
261
+ box-shadow: 0 0 0 5px rgba(161, 91, 46, 0.12);
262
+ }
263
+
264
+ [data-status="invalid"] .n-status-dot {
265
+ background: var(--n-bad);
266
+ box-shadow: 0 0 0 5px rgba(156, 56, 49, 0.12);
267
+ }
268
+
269
+ .n-status strong {
270
+ display: block;
271
+ font-size: 1.05rem;
272
+ }
273
+
274
+ .n-status p {
275
+ max-width: 240px;
276
+ margin: 5px 0 0;
277
+ color: var(--n-muted);
278
+ font-size: 0.78rem;
279
+ line-height: 1.45;
280
+ }
281
+
282
+ .n-output-list {
283
+ display: grid;
284
+ gap: 7px;
285
+ }
286
+
287
+ .n-output-row {
288
+ padding-bottom: 7px;
289
+ border-bottom: 1px solid var(--n-line);
290
+ color: var(--n-muted);
291
+ font-size: 0.75rem;
292
+ }
293
+
294
+ .n-output-row strong {
295
+ color: var(--n-ink);
296
+ font-size: 0.82rem;
297
+ text-align: right;
298
+ }
299
+
300
+ .n-page-band {
301
+ margin: 20px 0 0;
302
+ }
303
+
304
+ .n-page-band span {
305
+ color: var(--n-accent);
306
+ }
307
+
308
+ .n-scene-wrap {
309
+ display: grid;
310
+ grid-template-rows: auto 1fr auto;
311
+ min-height: 300px;
312
+ padding: 17px;
313
+ border: 1px solid var(--n-line);
314
+ background: var(--n-paper-alt);
315
+ }
316
+
317
+ .n-scene-label {
318
+ font-size: 0.65rem;
319
+ }
320
+
321
+ .n-scene {
322
+ min-height: 220px;
323
+ }
324
+
325
+ .n-spread-svg {
326
+ display: block;
327
+ width: 100%;
328
+ height: 100%;
329
+ min-height: 230px;
330
+ }
331
+
332
+ .n-paper {
333
+ fill: var(--n-paper);
334
+ stroke: var(--n-line);
335
+ stroke-width: 1;
336
+ }
337
+
338
+ .n-safe-zone {
339
+ fill: var(--n-accent-soft);
340
+ opacity: 0.72;
341
+ }
342
+
343
+ .n-crease {
344
+ fill: none;
345
+ stroke: var(--n-accent);
346
+ stroke-width: 2;
347
+ }
348
+
349
+ .n-rule {
350
+ fill: none;
351
+ stroke: var(--n-ink);
352
+ stroke-linecap: round;
353
+ stroke-width: 1.5;
354
+ opacity: 0.58;
355
+ }
356
+
357
+ .n-rule-soft {
358
+ opacity: 0.23;
359
+ }
360
+
361
+ .n-measure {
362
+ fill: none;
363
+ stroke: var(--n-accent);
364
+ stroke-dasharray: 2 3;
365
+ stroke-width: 1;
366
+ }
367
+
368
+ .n-measure-outer {
369
+ stroke-dasharray: none;
370
+ }
371
+
372
+ .n-measure-text,
373
+ .n-scene-status {
374
+ fill: var(--n-accent);
375
+ font-size: 7px;
376
+ font-weight: 700;
377
+ }
378
+
379
+ .n-scene-status-ready {
380
+ fill: var(--n-ok);
381
+ }
382
+
383
+ .n-scene-status-low-pages,
384
+ .n-scene-status-high-pages {
385
+ fill: var(--n-warn);
386
+ }
387
+
388
+ .n-scene-status-invalid {
389
+ fill: var(--n-bad);
390
+ }
391
+
392
+ .n-scene-hint {
393
+ margin: 8px 0 0;
394
+ color: var(--n-muted);
395
+ font-size: 0.73rem;
396
+ line-height: 1.45;
397
+ }
398
+
399
+ .n-source {
400
+ margin: 0;
401
+ padding-top: 9px;
402
+ border-top: 1px solid var(--n-line);
403
+ color: var(--n-muted);
404
+ font-size: 0.72rem;
405
+ line-height: 1.45;
406
+ }
407
+
408
+ .n-tool[data-paper="white-uncoated"] .n-paper {
409
+ fill: var(--n-paper-white);
410
+ }
411
+
412
+ .n-tool[data-paper="coated"] .n-paper {
413
+ fill: var(--n-paper-coated);
414
+ }
415
+
416
+ html.theme-dark .n-tool {
417
+ --n-ink: #f6efe5;
418
+ --n-muted: #b9afa2;
419
+ --n-paper: #25231f;
420
+ --n-paper-white: #f8f5ee;
421
+ --n-paper-coated: #e9eef0;
422
+ --n-paper-alt: #171613;
423
+ --n-line: #51483e;
424
+ --n-accent: #e49a72;
425
+ --n-accent-soft: #493129;
426
+ --n-ok: #82ba93;
427
+ --n-warn: #e0a36f;
428
+ --n-bad: #ee8b7f;
429
+ --n-shadow: rgba(0, 0, 0, 0.28);
430
+ }
431
+
432
+ @media (max-width: 780px) {
433
+ .n-tool {
434
+ grid-template-columns: 1fr;
435
+ max-width: calc(100vw - 40px);
436
+ }
437
+
438
+ .n-specs {
439
+ border-right: 0;
440
+ border-bottom: 1px solid var(--n-line);
441
+ }
442
+
443
+ .n-result-copy {
444
+ grid-template-columns: 1fr;
445
+ }
446
+ }
447
+
448
+ @media (max-width: 480px) {
449
+ .n-specs,
450
+ .n-result {
451
+ padding: 20px;
452
+ }
453
+
454
+ .n-field-grid,
455
+ .n-select-grid {
456
+ grid-template-columns: 1fr;
457
+ }
458
+
459
+ .n-field-grid .n-field-pages {
460
+ grid-column: auto;
461
+ }
462
+ }
@@ -0,0 +1,102 @@
1
+ ---
2
+ import { calculateInterior, DEFAULT_INPUTS } from './logic';
3
+ import { createSpreadMarkup } from './dom-views';
4
+ import type { BookInteriorUI } from './ui';
5
+
6
+ interface Props {
7
+ ui: BookInteriorUI;
8
+ }
9
+
10
+ const { ui } = Astro.props as Props;
11
+ const initialCalculation = calculateInterior(DEFAULT_INPUTS);
12
+ ---
13
+
14
+ <div class="n-tool" data-tool="book-interior-margin-and-gutter-planner" data-unit="metric">
15
+ <script is:inline type="application/json" data-ui set:html={JSON.stringify(ui)}></script>
16
+ <div class="n-specs">
17
+ <div class="n-specs-head">
18
+ <span class="n-eyebrow">{ui.setupLabel}</span>
19
+ <div class="n-unit-toggle" role="group" aria-label={`${ui.metricLabel} ${ui.imperialLabel}`}>
20
+ <button type="button" data-unit="metric" aria-pressed="true">{ui.metricLabel}</button>
21
+ <button type="button" data-unit="imperial" aria-pressed="false">{ui.imperialLabel}</button>
22
+ </div>
23
+ </div>
24
+ <div class="n-field-grid">
25
+ <label class="n-field n-field-pages">
26
+ <span>{ui.pagesLabel}</span>
27
+ <input data-input="number" data-field="pages" type="number" inputmode="numeric" min="1" max="2000" value={DEFAULT_INPUTS.pages} />
28
+ </label>
29
+ <label class="n-field">
30
+ <span>{ui.widthLabel} <small data-unit-label>mm</small></span>
31
+ <input data-input="number" data-field="widthMm" type="number" inputmode="decimal" min="20" step="0.1" value={DEFAULT_INPUTS.widthMm} />
32
+ </label>
33
+ <label class="n-field">
34
+ <span>{ui.heightLabel} <small data-unit-label>mm</small></span>
35
+ <input data-input="number" data-field="heightMm" type="number" inputmode="decimal" min="20" step="0.1" value={DEFAULT_INPUTS.heightMm} />
36
+ </label>
37
+ </div>
38
+ <div class="n-select-grid">
39
+ <div class="n-field n-select" data-select="binding" data-open="false">
40
+ <span>{ui.bindingLabel}</span>
41
+ <button class="n-select-trigger" type="button" data-select-trigger aria-haspopup="listbox">{ui.bindingPerfectLabel}</button>
42
+ <div class="n-select-menu" data-select-menu role="listbox" hidden>
43
+ <button type="button" data-select-option data-value="perfect-bound" role="option">{ui.bindingPerfectLabel}</button>
44
+ <button type="button" data-select-option data-value="hardcover" role="option">{ui.bindingHardcoverLabel}</button>
45
+ <button type="button" data-select-option data-value="saddle-stitched" role="option">{ui.bindingSaddleLabel}</button>
46
+ </div>
47
+ </div>
48
+ <div class="n-field n-select" data-select="paperType" data-open="false">
49
+ <span>{ui.paperLabel}</span>
50
+ <button class="n-select-trigger" type="button" data-select-trigger aria-haspopup="listbox">{ui.paperCreamLabel}</button>
51
+ <div class="n-select-menu" data-select-menu role="listbox" hidden>
52
+ <button type="button" data-select-option data-value="white-uncoated" role="option">{ui.paperWhiteLabel}</button>
53
+ <button type="button" data-select-option data-value="cream-uncoated" role="option">{ui.paperCreamLabel}</button>
54
+ <button type="button" data-select-option data-value="coated" role="option">{ui.paperCoatedLabel}</button>
55
+ </div>
56
+ </div>
57
+ </div>
58
+ <button class="n-bleed-toggle" type="button" data-bleed-toggle role="switch" aria-checked="false">
59
+ <span class="n-bleed-mark" aria-hidden="true"></span>
60
+ <span>{ui.bleedLabel}</span>
61
+ <strong data-bleed-state>{ui.bleedOff}</strong>
62
+ </button>
63
+ </div>
64
+
65
+ <div class="n-result" aria-label={ui.marginLabel}>
66
+ <div class="n-result-copy">
67
+ <div class="n-status" data-status="ready">
68
+ <span class="n-status-dot" aria-hidden="true"></span>
69
+ <div>
70
+ <strong data-status-title>{ui.statusReady}</strong>
71
+ <p data-status-detail>{ui.statusReadyDetail}</p>
72
+ </div>
73
+ </div>
74
+ <div class="n-output-list">
75
+ <div class="n-output-row"><span>{ui.gutterLabel}</span><strong data-output="gutter">12.7 mm</strong></div>
76
+ <div class="n-output-row"><span>{ui.outerLabel}</span><strong data-output="outer">6.4 mm</strong></div>
77
+ <div class="n-output-row"><span>{ui.safeWidthLabel}</span><strong data-output="safeWidth">132.9 mm</strong></div>
78
+ <div class="n-output-row"><span>{ui.safeHeightLabel}</span><strong data-output="safeHeight">216.2 mm</strong></div>
79
+ <div class="n-output-row"><span>{ui.normalizedPagesLabel}</span><strong data-output="pages">240</strong></div>
80
+ </div>
81
+ <p class="n-page-band">{ui.pageFormatLabel} <span data-output="band">151-300</span></p>
82
+ </div>
83
+ <div class="n-scene-wrap">
84
+ <div class="n-scene-label"><span>{ui.sceneLabel}</span><span>{ui.trimLabel}</span></div>
85
+ <div class="n-scene" data-scene set:html={createSpreadMarkup(initialCalculation, ui)}></div>
86
+ <p class="n-scene-hint">{ui.sceneHint}</p>
87
+ </div>
88
+ <p class="n-source">{ui.sourceLabel}</p>
89
+ </div>
90
+ </div>
91
+
92
+ <script>
93
+ import { initBookInteriorPlanner } from './controller';
94
+
95
+ const root = document.querySelector<HTMLElement>('[data-tool="book-interior-margin-and-gutter-planner"]');
96
+ const data = root?.querySelector<HTMLScriptElement>('[data-ui]');
97
+ if (root && data) initBookInteriorPlanner(root, JSON.parse(data.textContent || '{}'));
98
+ </script>
99
+
100
+ <style>
101
+ @import url('./book-interior-margin-and-gutter-planner.css');
102
+ </style>