@jjlmoya/utils-books 1.8.0 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/package.json +1 -1
  2. package/src/category/index.ts +2 -1
  3. package/src/entries.ts +4 -1
  4. package/src/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-index-page-budget-calculator/bibliography.astro +6 -0
  8. package/src/tool/book-index-page-budget-calculator/bibliography.ts +17 -0
  9. package/src/tool/book-index-page-budget-calculator/book-index-page-budget-calculator.css +478 -0
  10. package/src/tool/book-index-page-budget-calculator/component.astro +134 -0
  11. package/src/tool/book-index-page-budget-calculator/controller.ts +114 -0
  12. package/src/tool/book-index-page-budget-calculator/dom-views.ts +89 -0
  13. package/src/tool/book-index-page-budget-calculator/entry.ts +39 -0
  14. package/src/tool/book-index-page-budget-calculator/evaluator.ts +26 -0
  15. package/src/tool/book-index-page-budget-calculator/i18n/de.ts +35 -0
  16. package/src/tool/book-index-page-budget-calculator/i18n/en.ts +113 -0
  17. package/src/tool/book-index-page-budget-calculator/i18n/es.ts +35 -0
  18. package/src/tool/book-index-page-budget-calculator/i18n/fr.ts +8 -0
  19. package/src/tool/book-index-page-budget-calculator/i18n/id.ts +35 -0
  20. package/src/tool/book-index-page-budget-calculator/i18n/it.ts +8 -0
  21. package/src/tool/book-index-page-budget-calculator/i18n/ja.ts +8 -0
  22. package/src/tool/book-index-page-budget-calculator/i18n/ko.ts +8 -0
  23. package/src/tool/book-index-page-budget-calculator/i18n/nl.ts +8 -0
  24. package/src/tool/book-index-page-budget-calculator/i18n/pl.ts +8 -0
  25. package/src/tool/book-index-page-budget-calculator/i18n/pt.ts +8 -0
  26. package/src/tool/book-index-page-budget-calculator/i18n/ru.ts +8 -0
  27. package/src/tool/book-index-page-budget-calculator/i18n/sv.ts +8 -0
  28. package/src/tool/book-index-page-budget-calculator/i18n/tr.ts +8 -0
  29. package/src/tool/book-index-page-budget-calculator/i18n/zh.ts +8 -0
  30. package/src/tool/book-index-page-budget-calculator/index.ts +11 -0
  31. package/src/tool/book-index-page-budget-calculator/logic.test.ts +52 -0
  32. package/src/tool/book-index-page-budget-calculator/logic.ts +86 -0
  33. package/src/tool/book-index-page-budget-calculator/seo.astro +15 -0
  34. package/src/tool/book-index-page-budget-calculator/storage.ts +17 -0
  35. package/src/tool/book-index-page-budget-calculator/ui.ts +41 -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.8.0",
3
+ "version": "1.9.0",
4
4
  "type": "module",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -1,10 +1,11 @@
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
+ import { bookIndexPageBudgetCalculator } from '../tool/book-index-page-budget-calculator/entry';
3
4
  import type { CategoryLocaleContent, KnownLocale } from '../types';
4
5
 
5
6
  export const booksCategory = {
6
7
  icon: 'mdi:book-open-page-variant-outline',
7
- tools: [bookPaginationAndSpineCalculator, bookInteriorMarginAndGutterPlanner],
8
+ tools: [bookPaginationAndSpineCalculator, bookInteriorMarginAndGutterPlanner, bookIndexPageBudgetCalculator],
8
9
  i18n: {
9
10
  de: () => import('./i18n/de').then((module) => module.content),
10
11
  en: () => import('./i18n/en').then((module) => module.content),
package/src/entries.ts CHANGED
@@ -2,9 +2,12 @@ export { bookPaginationAndSpineCalculator } from './tool/book-pagination-and-spi
2
2
  export type { BookPaginationUI, BookPaginationLocaleContent } from './tool/book-pagination-and-spine-calculator/entry';
3
3
  export { bookReadingTimeDeadlinePlanner } from './tool/book-reading-time-deadline-planner/entry';
4
4
  export type { BookReadingUI, BookReadingLocaleContent } from './tool/book-reading-time-deadline-planner/entry';
5
+ export { bookIndexPageBudgetCalculator } from './tool/book-index-page-budget-calculator/entry';
6
+ export type { BookIndexUI, BookIndexLocaleContent } from './tool/book-index-page-budget-calculator/entry';
5
7
 
6
8
  import { bookPaginationAndSpineCalculator } from './tool/book-pagination-and-spine-calculator/entry';
7
9
  import { bookInteriorMarginAndGutterPlanner } from './tool/book-interior-margin-and-gutter-planner/entry';
8
10
  import { bookReadingTimeDeadlinePlanner } from './tool/book-reading-time-deadline-planner/entry';
11
+ import { bookIndexPageBudgetCalculator } from './tool/book-index-page-budget-calculator/entry';
9
12
 
10
- export const ALL_ENTRIES = [bookPaginationAndSpineCalculator, bookInteriorMarginAndGutterPlanner, bookReadingTimeDeadlinePlanner];
13
+ export const ALL_ENTRIES = [bookPaginationAndSpineCalculator, bookInteriorMarginAndGutterPlanner, bookReadingTimeDeadlinePlanner, bookIndexPageBudgetCalculator];
package/src/index.ts CHANGED
@@ -3,6 +3,7 @@ export const booksCategorySEO = () => import('./category/BooksCategorySEO.astro'
3
3
 
4
4
  export { bookPaginationAndSpineCalculator, BOOK_PAGINATION_AND_SPINE_CALCULATOR_TOOL } from './tool/book-pagination-and-spine-calculator';
5
5
  export { bookInteriorMarginAndGutterPlanner, BOOK_INTERIOR_MARGIN_AND_GUTTER_PLANNER_TOOL } from './tool/book-interior-margin-and-gutter-planner';
6
+ export { bookIndexPageBudgetCalculator, BOOK_INDEX_PAGE_BUDGET_CALCULATOR_TOOL } from './tool/book-index-page-budget-calculator';
6
7
 
7
8
  export type {
8
9
  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(3);
21
+ expect(ALL_TOOLS.length).toBe(4);
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(3);
8
+ expect(ALL_TOOLS.length).toBe(4);
9
9
  });
10
10
 
11
11
  it('booksCategory should be defined', () => {
@@ -0,0 +1,6 @@
1
+ ---
2
+ import { Bibliography } from '@jjlmoya/utils-shared';
3
+ import { BOOK_INDEX_BIBLIOGRAPHY } from './bibliography';
4
+ ---
5
+
6
+ <Bibliography links={BOOK_INDEX_BIBLIOGRAPHY} />
@@ -0,0 +1,17 @@
1
+ import type { BibliographyEntry } from '../../types';
2
+
3
+ export const BOOK_INDEX_BIBLIOGRAPHY: BibliographyEntry[] = [
4
+ {
5
+ name: 'Oxford University Press, Index',
6
+ url: 'https://academic.oup.com/pages/for-authors/books/the-book-publishing-process/writing-and-content-preparation/index',
7
+ },
8
+ {
9
+ name: 'CIMSUR UNAM, Criterios Editoriales',
10
+ url: 'https://libros.cimsur.unam.mx/index.php/cimsur/CriteriosEditoriales',
11
+ },
12
+ ];
13
+
14
+ export const BOOK_INDEX_BIBLIOGRAPHY_TRACEABILITY = [
15
+ { source: 'Oxford University Press', region: 'United Kingdom', language: 'English', supports: 'An index is back matter, should reflect reader use, and is finalized against typeset material.' },
16
+ { source: 'CIMSUR UNAM', region: 'Mexico', language: 'Spanish', supports: 'Analytical indexes are alphabetic editorial matter and should be included in the work plan.' },
17
+ ] as const;
@@ -0,0 +1,478 @@
1
+ .book-index-tool {
2
+ --n-page: #f4ecdf;
3
+ --n-surface: #fffaf3;
4
+ --n-paper: #fffdf8;
5
+ --n-ink: #201c18;
6
+ --n-muted: #74685c;
7
+ --n-line: #d9cbbb;
8
+ --n-accent: #c65334;
9
+ --n-accent-soft: #f3d9c8;
10
+ --n-body: #d58a49;
11
+ --n-index: #27766d;
12
+ --n-good: #27724d;
13
+ --n-warn: #b64b32;
14
+
15
+ background: var(--n-surface);
16
+ border: 1px solid var(--n-line);
17
+ color: var(--n-ink);
18
+ margin: 0 auto;
19
+ max-width: 1180px;
20
+ min-width: 0;
21
+ overflow: hidden;
22
+ width: 100%;
23
+ }
24
+
25
+ .book-index-tool,
26
+ .book-index-tool *,
27
+ .book-index-tool *::before,
28
+ .book-index-tool *::after {
29
+ box-sizing: border-box;
30
+ }
31
+
32
+ .theme-dark .book-index-tool {
33
+ --n-page: #111820;
34
+ --n-surface: #1a222d;
35
+ --n-paper: #242f3b;
36
+ --n-ink: #edf2f4;
37
+ --n-muted: #adbac6;
38
+ --n-line: #3a4756;
39
+ --n-accent: #e3ab82;
40
+ --n-accent-soft: #473a35;
41
+ --n-body: #b98a64;
42
+ --n-index: #79aaa1;
43
+ --n-good: #8fbc9c;
44
+ --n-warn: #d58b78;
45
+ }
46
+
47
+ .index-workbench {
48
+ background: var(--n-page);
49
+ min-width: 0;
50
+ }
51
+
52
+ .budget-summary {
53
+ background: var(--n-surface);
54
+ border-bottom: 1px solid var(--n-line);
55
+ padding: clamp(1rem, 2.4vw, 1.65rem);
56
+ }
57
+
58
+ .summary-topline {
59
+ align-items: center;
60
+ display: flex;
61
+ gap: 1rem;
62
+ justify-content: space-between;
63
+ margin-bottom: 1rem;
64
+ }
65
+
66
+ .summary-topline > span {
67
+ color: var(--n-muted);
68
+ font-size: 0.74rem;
69
+ font-weight: 800;
70
+ letter-spacing: 0.1em;
71
+ text-transform: uppercase;
72
+ }
73
+
74
+ .summary-topline strong {
75
+ color: var(--n-accent);
76
+ font-size: 0.9rem;
77
+ }
78
+
79
+ .summary-grid {
80
+ align-items: stretch;
81
+ display: grid;
82
+ gap: 1.25rem;
83
+ grid-template-columns: repeat(auto-fit, minmax(min(100%, 260px), 1fr));
84
+ }
85
+
86
+ .summary-total,
87
+ .budget-strip,
88
+ .summary-brief {
89
+ min-width: 0;
90
+ }
91
+
92
+ .summary-total {
93
+ border-right: 1px solid var(--n-line);
94
+ display: flex;
95
+ flex-direction: column;
96
+ justify-content: center;
97
+ padding-right: 1.25rem;
98
+ }
99
+
100
+ .summary-total > span,
101
+ .summary-total small,
102
+ .strip-labels,
103
+ .strip-target {
104
+ color: var(--n-muted);
105
+ font-size: 0.73rem;
106
+ font-weight: 800;
107
+ letter-spacing: 0.04em;
108
+ text-transform: uppercase;
109
+ }
110
+
111
+ .summary-total > strong {
112
+ color: var(--n-accent);
113
+ font-size: clamp(3rem, 7vw, 5.2rem);
114
+ letter-spacing: -0.07em;
115
+ line-height: 0.92;
116
+ margin: 0.35rem 0 0.55rem;
117
+ }
118
+
119
+ .summary-total small {
120
+ font-size: 0.72rem;
121
+ letter-spacing: 0;
122
+ text-transform: none;
123
+ }
124
+
125
+ .summary-total small b,
126
+ .strip-labels b,
127
+ .strip-target strong {
128
+ color: var(--n-ink);
129
+ }
130
+
131
+ .budget-strip {
132
+ display: flex;
133
+ flex-direction: column;
134
+ justify-content: center;
135
+ min-width: 0;
136
+ }
137
+
138
+ .strip-labels,
139
+ .strip-target {
140
+ align-items: center;
141
+ display: flex;
142
+ justify-content: space-between;
143
+ gap: 0.75rem;
144
+ }
145
+
146
+ .strip-labels span:last-child {
147
+ color: var(--n-index);
148
+ }
149
+
150
+ .strip-track {
151
+ background: var(--n-line);
152
+ display: flex;
153
+ height: clamp(2.6rem, 6vw, 4rem);
154
+ margin: 0.6rem 0 0.55rem;
155
+ min-width: 0;
156
+ overflow: hidden;
157
+ }
158
+
159
+ .strip-body,
160
+ .strip-index {
161
+ display: block;
162
+ min-width: 1rem;
163
+ transition: width 0.25s ease, background-color 0.25s ease;
164
+ }
165
+
166
+ .strip-body {
167
+ background: var(--n-body);
168
+ width: var(--paper-share, 90%);
169
+ }
170
+
171
+ .strip-index {
172
+ background: var(--n-index);
173
+ width: var(--paper-share, 10%);
174
+ }
175
+
176
+ .strip-target {
177
+ border-top: 1px dashed var(--n-muted);
178
+ padding-top: 0.55rem;
179
+ }
180
+
181
+ .strip-target strong {
182
+ font-size: 0.9rem;
183
+ }
184
+
185
+ .summary-brief {
186
+ align-self: center;
187
+ border-left: 3px solid var(--n-good);
188
+ min-width: 0;
189
+ overflow-wrap: anywhere;
190
+ padding-left: 0.9rem;
191
+ }
192
+
193
+ [data-status='tight'] .summary-brief { border-left-color: var(--n-warn); }
194
+ [data-status='roomy'] .summary-brief { border-left-color: var(--n-index); }
195
+
196
+ .status-dot {
197
+ background: var(--n-good);
198
+ border-radius: 50%;
199
+ display: inline-block;
200
+ height: 0.55rem;
201
+ margin-right: 0.4rem;
202
+ vertical-align: 0.05em;
203
+ width: 0.55rem;
204
+ }
205
+
206
+ [data-status='tight'] .status-dot { background: var(--n-warn); }
207
+ [data-status='roomy'] .status-dot { background: var(--n-index); }
208
+
209
+ .summary-brief strong {
210
+ color: var(--n-ink);
211
+ font-size: 0.78rem;
212
+ }
213
+
214
+ .summary-brief p {
215
+ color: var(--n-muted);
216
+ font-size: 0.82rem;
217
+ line-height: 1.45;
218
+ margin: 0.45rem 0 0;
219
+ }
220
+
221
+ .workbench-controls {
222
+ display: grid;
223
+ gap: 1px;
224
+ grid-template-columns: repeat(auto-fit, minmax(min(100%, 300px), 1fr));
225
+ padding: 1px;
226
+ }
227
+
228
+ .control-block {
229
+ background: var(--n-surface);
230
+ display: grid;
231
+ gap: 1.15rem;
232
+ min-width: 0;
233
+ padding: clamp(1rem, 2.4vw, 1.45rem);
234
+ }
235
+
236
+ .field,
237
+ .dimension-field,
238
+ .unit-field,
239
+ .density-field {
240
+ min-width: 0;
241
+ }
242
+
243
+ .field label,
244
+ .field-row label,
245
+ .unit-field legend,
246
+ .density-field legend {
247
+ color: var(--n-ink);
248
+ display: block;
249
+ font-size: 0.75rem;
250
+ font-weight: 800;
251
+ letter-spacing: 0.04em;
252
+ margin-bottom: 0.45rem;
253
+ text-transform: uppercase;
254
+ }
255
+
256
+ .field-row {
257
+ display: grid;
258
+ gap: 0.55rem;
259
+ grid-template-columns: 1fr 1fr;
260
+ }
261
+
262
+ .field-row label:last-child {
263
+ grid-column: 2;
264
+ grid-row: 1;
265
+ }
266
+
267
+ input {
268
+ background: var(--n-paper);
269
+ border: 1px solid var(--n-line);
270
+ border-radius: 0;
271
+ color: var(--n-ink);
272
+ font: inherit;
273
+ font-size: 1.05rem;
274
+ min-height: 2.8rem;
275
+ min-width: 0;
276
+ padding: 0.65rem 0.75rem;
277
+ width: 100%;
278
+ }
279
+
280
+ input:focus-visible,
281
+ button:focus-visible {
282
+ outline: 3px solid var(--n-accent);
283
+ outline-offset: 2px;
284
+ }
285
+
286
+ .input-row {
287
+ align-items: center;
288
+ display: grid;
289
+ gap: 0.45rem;
290
+ grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
291
+ }
292
+
293
+ .times {
294
+ color: var(--n-muted);
295
+ font-size: 1.1rem;
296
+ font-weight: 800;
297
+ }
298
+
299
+ .field-hint {
300
+ color: var(--n-muted);
301
+ display: block;
302
+ font-size: 0.74rem;
303
+ line-height: 1.35;
304
+ margin-top: 0.4rem;
305
+ }
306
+
307
+ fieldset {
308
+ border: 0;
309
+ margin: 0;
310
+ padding: 0;
311
+ }
312
+
313
+ .unit-switch,
314
+ .density-options {
315
+ display: grid;
316
+ gap: 0.45rem;
317
+ grid-template-columns: repeat(2, minmax(0, 1fr));
318
+ }
319
+
320
+ .density-options {
321
+ grid-template-columns: repeat(3, minmax(0, 1fr));
322
+ }
323
+
324
+ button {
325
+ background: var(--n-paper);
326
+ border: 1px solid var(--n-line);
327
+ border-radius: 0;
328
+ color: var(--n-ink);
329
+ cursor: pointer;
330
+ font: inherit;
331
+ min-height: 2.8rem;
332
+ min-width: 0;
333
+ padding: 0.55rem 0.6rem;
334
+ }
335
+
336
+ button[aria-pressed='true'] {
337
+ background: var(--n-accent-soft);
338
+ border-color: var(--n-accent);
339
+ color: var(--n-ink);
340
+ font-weight: 800;
341
+ }
342
+
343
+ .density-field .field-hint {
344
+ line-height: 1.2;
345
+ margin: 0 0 0.45rem;
346
+ }
347
+
348
+ .density-options button {
349
+ align-items: center;
350
+ display: flex;
351
+ gap: 0.35rem;
352
+ justify-content: center;
353
+ overflow: hidden;
354
+ text-overflow: ellipsis;
355
+ white-space: nowrap;
356
+ }
357
+
358
+ .density-mark {
359
+ border: 1px solid var(--n-accent);
360
+ display: inline-block;
361
+ flex: 0 0 auto;
362
+ height: 0.75rem;
363
+ width: 0.75rem;
364
+ }
365
+
366
+ .airy-mark { background: var(--n-paper); }
367
+ .standard-mark { background: var(--n-accent-soft); }
368
+ .compact-mark { background: var(--n-accent); }
369
+
370
+ .reading-details {
371
+ background: var(--n-surface);
372
+ border-top: 1px solid var(--n-line);
373
+ display: grid;
374
+ gap: 0;
375
+ grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
376
+ padding: 1rem clamp(1rem, 2.4vw, 1.45rem);
377
+ }
378
+
379
+ .reading-item {
380
+ border-right: 1px solid var(--n-line);
381
+ display: flex;
382
+ flex-direction: column;
383
+ gap: 0.25rem;
384
+ min-width: 0;
385
+ padding: 0 0.9rem;
386
+ }
387
+
388
+ .reading-item:first-child {
389
+ padding-left: 0;
390
+ }
391
+
392
+ .reading-item:last-child {
393
+ border-right: 0;
394
+ padding-right: 0;
395
+ }
396
+
397
+ .reading-item span {
398
+ color: var(--n-muted);
399
+ font-size: 0.68rem;
400
+ font-weight: 800;
401
+ letter-spacing: 0.04em;
402
+ line-height: 1.25;
403
+ text-transform: uppercase;
404
+ }
405
+
406
+ .reading-item strong {
407
+ color: var(--n-ink);
408
+ font-size: 1.15rem;
409
+ overflow-wrap: anywhere;
410
+ }
411
+
412
+ .source-note {
413
+ color: var(--n-muted);
414
+ font-size: 0.77rem;
415
+ line-height: 1.5;
416
+ margin: 0;
417
+ padding: 0 clamp(1rem, 2.4vw, 1.45rem) 1.25rem;
418
+ }
419
+
420
+ @media (max-width: 900px) {
421
+ .summary-grid {
422
+ grid-template-columns: minmax(130px, 0.65fr) minmax(240px, 1.35fr);
423
+ }
424
+
425
+ .summary-brief {
426
+ grid-column: 1 / -1;
427
+ }
428
+
429
+ .workbench-controls {
430
+ grid-template-columns: 1fr 1fr;
431
+ }
432
+
433
+ .control-block:last-child {
434
+ grid-column: 1 / -1;
435
+ }
436
+
437
+ .reading-details {
438
+ grid-template-columns: repeat(3, minmax(0, 1fr));
439
+ row-gap: 1rem;
440
+ }
441
+
442
+ .reading-item:nth-child(3) {
443
+ border-right: 0;
444
+ }
445
+
446
+ .reading-item:nth-child(4) {
447
+ padding-left: 0;
448
+ }
449
+ }
450
+
451
+ @media (max-width: 600px) {
452
+ .summary-grid,
453
+ .workbench-controls,
454
+ .reading-details { grid-template-columns: 1fr; }
455
+ .summary-total {
456
+ border-bottom: 1px solid var(--n-line);
457
+ border-right: 0;
458
+ padding: 0 0 1rem;
459
+ }
460
+
461
+ .summary-total > strong {
462
+ font-size: 3.7rem;
463
+ }
464
+ .summary-brief,
465
+ .control-block:last-child { grid-column: auto; }
466
+ .control-block { gap: 1rem; }
467
+ .reading-item,
468
+ .reading-item:nth-child(3),
469
+ .reading-item:nth-child(4) {
470
+ border-bottom: 1px solid var(--n-line);
471
+ border-right: 0;
472
+ padding: 0 0 0.7rem;
473
+ }
474
+ .reading-item:last-child {
475
+ border-bottom: 0;
476
+ padding-bottom: 0;
477
+ }
478
+ }
@@ -0,0 +1,134 @@
1
+ ---
2
+ import { calculateIndexBudget, DEFAULT_INDEX_INPUTS } from './logic';
3
+ import { evaluateBudget } from './evaluator';
4
+ import type { BookIndexUI } from './ui';
5
+
6
+ interface Props {
7
+ ui: BookIndexUI;
8
+ }
9
+
10
+ const { ui } = Astro.props as Props;
11
+ const initialBudget = calculateIndexBudget(DEFAULT_INDEX_INPUTS);
12
+ const initialEvaluation = evaluateBudget(initialBudget);
13
+ function initialStatusCopy(status: ReturnType<typeof evaluateBudget>['status'], content: BookIndexUI): { text: string; hint: string } {
14
+ if (status === 'tight') return { text: content.statusTight, hint: content.statusTightHint };
15
+ if (status === 'roomy') return { text: content.statusRoomy, hint: content.statusRoomyHint };
16
+ return { text: content.statusBalanced, hint: content.statusBalancedHint };
17
+ }
18
+
19
+ function initialGapCopy(gap: number, content: BookIndexUI): string {
20
+ if (gap > 0) return `${gap} ${content.summaryPagesOver}`;
21
+ if (gap < 0) return `${Math.abs(gap)} ${content.summaryPagesSpare}`;
22
+ return content.summaryOnTarget;
23
+ }
24
+
25
+ const initialStatusCopyValue = initialStatusCopy(initialEvaluation.status, ui);
26
+ const initialGap = initialBudget.estimatedIndexPages - initialBudget.targetIndexPages;
27
+ const initialGapLabel = initialGapCopy(initialGap, ui);
28
+ const initialBodyShare = (initialBudget.textPages / initialBudget.totalPages) * 100;
29
+ const initialIndexShare = 100 - initialBodyShare;
30
+ ---
31
+
32
+ <div class="book-index-tool" data-book-index-tool data-ui={JSON.stringify(ui)} data-status={initialEvaluation.status} data-units="imperial">
33
+ <form class="index-workbench" data-form>
34
+ <section class="budget-summary" aria-label={ui.summaryLabel}>
35
+ <div class="summary-topline">
36
+ <span>{ui.summaryLabel}</span>
37
+ <strong data-output="summary-gap">{initialGapLabel}</strong>
38
+ </div>
39
+ <div class="summary-grid">
40
+ <div class="summary-total">
41
+ <span>{ui.totalPagesLabel}</span>
42
+ <strong data-output="total-pages">{initialBudget.totalPages}</strong>
43
+ <small>{ui.targetTotalPagesLabel} <b data-output="target-total-pages">{initialBudget.targetTotalPages}</b></small>
44
+ </div>
45
+ <div class="budget-strip" data-scene aria-label={ui.sceneLabel}>
46
+ <div class="strip-labels">
47
+ <span>{ui.sceneBodyLabel} <b data-output="scene-body-count">{initialBudget.textPages}</b></span>
48
+ <span>{ui.sceneIndexLabel} <b data-output="scene-index-count">{initialBudget.estimatedIndexPages}</b></span>
49
+ </div>
50
+ <div class="strip-track">
51
+ <span class="strip-body" data-paper="body" style={`--paper-share: ${initialBodyShare}%`}></span>
52
+ <span class="strip-index" data-paper="index" style={`--paper-share: ${initialIndexShare}%`}></span>
53
+ </div>
54
+ <div class="strip-target"><span>{ui.targetIndexPagesLabel}</span><strong data-output="summary-target">{initialBudget.targetIndexPages}</strong></div>
55
+ </div>
56
+ <div class="summary-brief" data-status-line>
57
+ <span class="status-dot"></span>
58
+ <strong data-output="status">{initialStatusCopyValue.text}</strong>
59
+ <p data-output="status-hint">{initialStatusCopyValue.hint}</p>
60
+ </div>
61
+ </div>
62
+ </section>
63
+
64
+ <div class="workbench-controls">
65
+ <div class="control-block">
66
+ <div class="field">
67
+ <label for="manuscript-words">{ui.wordsLabel}</label>
68
+ <input id="manuscript-words" data-input="manuscript-words" type="number" min="100" max="500000" step="100" value={DEFAULT_INDEX_INPUTS.manuscriptWords} inputmode="numeric" />
69
+ <span class="field-hint">{ui.wordsHint}</span>
70
+ </div>
71
+ <div class="field">
72
+ <label for="index-entries">{ui.indexEntriesLabel}</label>
73
+ <input id="index-entries" data-input="index-entries" type="number" min="10" max="10000" step="10" value={DEFAULT_INDEX_INPUTS.indexEntries} inputmode="numeric" />
74
+ <span class="field-hint">{ui.indexEntriesHint}</span>
75
+ </div>
76
+ </div>
77
+
78
+ <div class="control-block">
79
+ <div class="dimension-field">
80
+ <div class="field-row">
81
+ <label for="trim-width">{ui.trimWidthLabel}</label>
82
+ <label for="trim-height">{ui.trimHeightLabel}</label>
83
+ </div>
84
+ <div class="input-row">
85
+ <input id="trim-width" data-input="trim-width" type="number" min="3" max="15" step="0.01" value={DEFAULT_INDEX_INPUTS.trimWidthInches} inputmode="decimal" />
86
+ <span class="times">×</span>
87
+ <input id="trim-height" data-input="trim-height" type="number" min="3" max="15" step="0.01" value={DEFAULT_INDEX_INPUTS.trimHeightInches} inputmode="decimal" />
88
+ </div>
89
+ <span class="field-hint">{ui.trimHint}</span>
90
+ </div>
91
+ <fieldset class="unit-field">
92
+ <legend>{ui.unitLabel}</legend>
93
+ <div class="unit-switch">
94
+ <button type="button" data-unit="imperial" aria-pressed="true">{ui.imperialButtonLabel}</button>
95
+ <button type="button" data-unit="metric" aria-pressed="false">{ui.metricButtonLabel}</button>
96
+ </div>
97
+ </fieldset>
98
+ </div>
99
+
100
+ <div class="control-block">
101
+ <fieldset class="density-field">
102
+ <legend>{ui.densityLabel}</legend>
103
+ <span class="field-hint">{ui.densityHint}</span>
104
+ <div class="density-options">
105
+ <button type="button" data-density="airy" aria-pressed="false"><span class="density-mark airy-mark"></span>{ui.airyLabel}</button>
106
+ <button type="button" data-density="standard" aria-pressed="true"><span class="density-mark standard-mark"></span>{ui.standardLabel}</button>
107
+ <button type="button" data-density="compact" aria-pressed="false"><span class="density-mark compact-mark"></span>{ui.compactLabel}</button>
108
+ </div>
109
+ </fieldset>
110
+ <div class="field">
111
+ <label for="target-index-pages">{ui.targetIndexPagesLabel}</label>
112
+ <input id="target-index-pages" data-input="target-index-pages" type="number" min="1" max="200" step="1" value={DEFAULT_INDEX_INPUTS.targetIndexPages} inputmode="numeric" />
113
+ <span class="field-hint">{ui.targetIndexPagesHint}</span>
114
+ </div>
115
+ </div>
116
+ </div>
117
+
118
+ <div class="reading-details">
119
+ <div class="reading-item"><span>{ui.textPagesLabel}</span><strong data-output="text-pages">{initialBudget.textPages}</strong></div>
120
+ <div class="reading-item"><span>{ui.estimatedIndexPagesLabel}</span><strong data-output="index-pages">{initialBudget.estimatedIndexPages}</strong></div>
121
+ <div class="reading-item"><span>{ui.entriesPerPageLabel}</span><strong data-output="entries-per-page">{initialBudget.targetEntriesPerPage.toFixed(1)}</strong></div>
122
+ <div class="reading-item"><span>{ui.indexShareLabel}</span><strong data-output="index-share">{initialBudget.indexSharePercent.toFixed(1)}%</strong></div>
123
+ <div class="reading-item"><span>{ui.densityLabel}</span><strong data-output="words-per-page">{initialBudget.wordsPerPage} {ui.wordsLabel.toLowerCase()}</strong></div>
124
+ </div>
125
+ <p class="source-note">{ui.sourceNote}</p>
126
+ </form>
127
+ </div>
128
+
129
+ <script>
130
+ import { initBookIndexTool } from './controller';
131
+
132
+ const root = document.querySelector<HTMLElement>('[data-book-index-tool]');
133
+ if (root) initBookIndexTool(root);
134
+ </script>