@jjlmoya/utils-books 1.7.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 (70) hide show
  1. package/package.json +1 -1
  2. package/src/category/index.ts +2 -1
  3. package/src/entries.ts +7 -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/tool/book-reading-time-deadline-planner/bibliography.astro +16 -0
  37. package/src/tool/book-reading-time-deadline-planner/bibliography.ts +31 -0
  38. package/src/tool/book-reading-time-deadline-planner/book-reading-time-deadline-planner.css +449 -0
  39. package/src/tool/book-reading-time-deadline-planner/calendar.test.ts +18 -0
  40. package/src/tool/book-reading-time-deadline-planner/calendar.ts +56 -0
  41. package/src/tool/book-reading-time-deadline-planner/component.astro +91 -0
  42. package/src/tool/book-reading-time-deadline-planner/controller.ts +253 -0
  43. package/src/tool/book-reading-time-deadline-planner/dom-views.ts +60 -0
  44. package/src/tool/book-reading-time-deadline-planner/entry.ts +27 -0
  45. package/src/tool/book-reading-time-deadline-planner/evaluator.ts +17 -0
  46. package/src/tool/book-reading-time-deadline-planner/i18n/de.ts +45 -0
  47. package/src/tool/book-reading-time-deadline-planner/i18n/en.ts +51 -0
  48. package/src/tool/book-reading-time-deadline-planner/i18n/es.ts +45 -0
  49. package/src/tool/book-reading-time-deadline-planner/i18n/fr.ts +45 -0
  50. package/src/tool/book-reading-time-deadline-planner/i18n/id.ts +45 -0
  51. package/src/tool/book-reading-time-deadline-planner/i18n/it.ts +45 -0
  52. package/src/tool/book-reading-time-deadline-planner/i18n/ja.ts +45 -0
  53. package/src/tool/book-reading-time-deadline-planner/i18n/ko.ts +45 -0
  54. package/src/tool/book-reading-time-deadline-planner/i18n/nl.ts +45 -0
  55. package/src/tool/book-reading-time-deadline-planner/i18n/pl.ts +45 -0
  56. package/src/tool/book-reading-time-deadline-planner/i18n/pt.ts +45 -0
  57. package/src/tool/book-reading-time-deadline-planner/i18n/ru.ts +45 -0
  58. package/src/tool/book-reading-time-deadline-planner/i18n/sv.ts +45 -0
  59. package/src/tool/book-reading-time-deadline-planner/i18n/tr.ts +45 -0
  60. package/src/tool/book-reading-time-deadline-planner/i18n/zh.ts +45 -0
  61. package/src/tool/book-reading-time-deadline-planner/index.ts +11 -0
  62. package/src/tool/book-reading-time-deadline-planner/logic.test.ts +61 -0
  63. package/src/tool/book-reading-time-deadline-planner/logic.ts +129 -0
  64. package/src/tool/book-reading-time-deadline-planner/seo.astro +15 -0
  65. package/src/tool/book-reading-time-deadline-planner/speed-test.test.ts +15 -0
  66. package/src/tool/book-reading-time-deadline-planner/speed-test.ts +9 -0
  67. package/src/tool/book-reading-time-deadline-planner/storage.ts +24 -0
  68. package/src/tool/book-reading-time-deadline-planner/ui.ts +76 -0
  69. package/src/tool/book-reading-time-deadline-planner/validation.ts +19 -0
  70. 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.7.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
@@ -1,7 +1,13 @@
1
1
  export { bookPaginationAndSpineCalculator } from './tool/book-pagination-and-spine-calculator/entry';
2
2
  export type { BookPaginationUI, BookPaginationLocaleContent } from './tool/book-pagination-and-spine-calculator/entry';
3
+ export { bookReadingTimeDeadlinePlanner } from './tool/book-reading-time-deadline-planner/entry';
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';
3
7
 
4
8
  import { bookPaginationAndSpineCalculator } from './tool/book-pagination-and-spine-calculator/entry';
5
9
  import { bookInteriorMarginAndGutterPlanner } from './tool/book-interior-margin-and-gutter-planner/entry';
10
+ import { bookReadingTimeDeadlinePlanner } from './tool/book-reading-time-deadline-planner/entry';
11
+ import { bookIndexPageBudgetCalculator } from './tool/book-index-page-budget-calculator/entry';
6
12
 
7
- export const ALL_ENTRIES = [bookPaginationAndSpineCalculator, bookInteriorMarginAndGutterPlanner];
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(2);
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(2);
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>