|
1 | 1 | <script setup> |
2 | | -import { computed, watch, ref, nextTick, onMounted, onBeforeUnmount } from 'vue'; |
| 2 | +import { computed, watch, ref, nextTick, onMounted, onBeforeUnmount, inject } from 'vue'; |
3 | 3 | import { t, uiLanguage } from '../../i18n/index.mjs'; |
4 | 4 | import { annotationSections, annotationChapters } from '../annotations/annotation-browser.mjs'; |
5 | 5 | import { platform, AppSegmentedControl, AppSegment } from '../../ui/controls.mjs'; |
@@ -79,8 +79,28 @@ const visibleOutline = computed(() => |
79 | 79 | ), |
80 | 80 | ), |
81 | 81 | ); |
| 82 | +// Experimental: first sentences of body paragraphs, keyed by the outline entry |
| 83 | +// that owns them. The optional Jev classifier can reject non-topic sentences. |
| 84 | +const outlineTopicSentences = inject('outlineTopicSentences', null); |
| 85 | +const topicSentenceClassifier = inject('topicSentenceClassifier', null); |
| 86 | +const topicSentences = computed(() => { |
| 87 | + const sections = outlineTopicSentences?.value; |
| 88 | + if (!sections?.size || props.mode !== 'outline') return new Map(); |
| 89 | + const classify = topicSentenceClassifier?.isTopicSentence; |
| 90 | + void topicSentenceClassifier?.revision?.value; |
| 91 | + if (typeof classify !== 'function') return sections; |
| 92 | + const kept = new Map(); |
| 93 | + for (const [id, items] of sections) { |
| 94 | + const accepted = items.filter((item) => classify(item.paragraph, item.sentence) !== false); |
| 95 | + if (accepted.length) kept.set(id, accepted); |
| 96 | + } |
| 97 | + return kept; |
| 98 | +}); |
82 | 99 | function hasChildren(item) { |
83 | | - return props.outline.some((child) => child.id.startsWith(item.id + '.')); |
| 100 | + return ( |
| 101 | + topicSentences.value.has(item.id) || |
| 102 | + props.outline.some((child) => child.id.startsWith(item.id + '.')) |
| 103 | + ); |
84 | 104 | } |
85 | 105 | function toggleOutline(item) { |
86 | 106 | const next = new Set(collapsed.value); |
@@ -289,37 +309,52 @@ watch( |
289 | 309 | </button> |
290 | 310 | </template> |
291 | 311 | </div> |
292 | | - <div |
293 | | - v-for="item in visibleOutline" |
294 | | - :key="item.id" |
295 | | - class="sidebar-outline-row" |
296 | | - :style="{ paddingLeft: 6 + Math.min(item.depth, 8) * 12 + 'px' }" |
297 | | - > |
298 | | - <button |
299 | | - v-if="hasChildren(item)" |
300 | | - class="sidebar-outline-toggle" |
301 | | - :aria-expanded="!collapsed.has(item.id)" |
302 | | - :aria-label=" |
303 | | - t(collapsed.has(item.id) ? 'sidebar.expand' : 'sidebar.collapse', { title: item.title }) |
304 | | - " |
305 | | - @click="toggleOutline(item)" |
| 312 | + <template v-for="item in visibleOutline" :key="item.id" |
| 313 | + ><div |
| 314 | + class="sidebar-outline-row" |
| 315 | + :style="{ paddingLeft: 6 + Math.min(item.depth, 8) * 12 + 'px' }" |
306 | 316 | > |
307 | | - <svg viewBox="0 0 12 12" aria-hidden="true"><path d="m4 2 4 4-4 4" /></svg></button |
308 | | - ><span v-else class="sidebar-outline-spacer"></span> |
309 | | - <button |
310 | | - class="sidebar-outline-item" |
311 | | - :class="{ |
312 | | - 'sidebar-outline-top-level': hasNestedOutline && item.depth === 0, |
313 | | - 'sidebar-outline-read': readChapters.has(item.id), |
314 | | - }" |
315 | | - :disabled="!item.page && !hasChildren(item)" |
316 | | - @click="item.page && $event.detail < 2 && emit('page', item.page)" |
317 | | - @dblclick="hasChildren(item) && toggleOutline(item)" |
318 | | - > |
319 | | - <span>{{ item.title }}</span |
320 | | - ><small v-if="item.page">{{ item.page }}</small> |
321 | | - </button> |
322 | | - </div> |
| 317 | + <button |
| 318 | + v-if="hasChildren(item)" |
| 319 | + class="sidebar-outline-toggle" |
| 320 | + :aria-expanded="!collapsed.has(item.id)" |
| 321 | + :aria-label=" |
| 322 | + t(collapsed.has(item.id) ? 'sidebar.expand' : 'sidebar.collapse', { |
| 323 | + title: item.title, |
| 324 | + }) |
| 325 | + " |
| 326 | + @click="toggleOutline(item)" |
| 327 | + > |
| 328 | + <svg viewBox="0 0 12 12" aria-hidden="true"><path d="m4 2 4 4-4 4" /></svg></button |
| 329 | + ><span v-else class="sidebar-outline-spacer"></span> |
| 330 | + <button |
| 331 | + class="sidebar-outline-item" |
| 332 | + :class="{ |
| 333 | + 'sidebar-outline-top-level': hasNestedOutline && item.depth === 0, |
| 334 | + 'sidebar-outline-read': readChapters.has(item.id), |
| 335 | + }" |
| 336 | + :disabled="!item.page && !hasChildren(item)" |
| 337 | + @click="item.page && $event.detail < 2 && emit('page', item.page)" |
| 338 | + @dblclick="hasChildren(item) && toggleOutline(item)" |
| 339 | + > |
| 340 | + <span>{{ item.title }}</span |
| 341 | + ><small v-if="item.page">{{ item.page }}</small> |
| 342 | + </button> |
| 343 | + </div> |
| 344 | + <template v-if="!collapsed.has(item.id)"> |
| 345 | + <button |
| 346 | + v-for="topic in topicSentences.get(item.id)" |
| 347 | + :key="topic.id" |
| 348 | + class="sidebar-outline-topic" |
| 349 | + :style="{ paddingLeft: 28 + Math.min(item.depth + 1, 8) * 12 + 'px' }" |
| 350 | + :title="topic.sentence" |
| 351 | + @click="emit('page', topic.page)" |
| 352 | + > |
| 353 | + <span>{{ topic.sentence }}</span |
| 354 | + ><small>{{ topic.page }}</small> |
| 355 | + </button> |
| 356 | + </template></template |
| 357 | + > |
323 | 358 | </div> |
324 | 359 | <div v-show="mode === 'annotations'" class="sidebar-annotations-view"> |
325 | 360 | <div class="annotation-browser-controls"> |
@@ -616,6 +651,36 @@ watch( |
616 | 651 | .sidebar-outline-top-level span { |
617 | 652 | font-weight: 700; |
618 | 653 | } |
| 654 | +.sidebar-outline-topic { |
| 655 | + display: flex; |
| 656 | + gap: 8px; |
| 657 | + width: 100%; |
| 658 | + box-sizing: border-box; |
| 659 | + border: 0; |
| 660 | + border-radius: 6px; |
| 661 | + background: transparent; |
| 662 | + padding: 4px 8px; |
| 663 | + color: var(--text-secondary); |
| 664 | + font-size: 12px; |
| 665 | + line-height: 1.4; |
| 666 | + text-align: left; |
| 667 | + cursor: pointer; |
| 668 | +} |
| 669 | +.sidebar-outline-topic span { |
| 670 | + flex: 1; |
| 671 | + overflow: hidden; |
| 672 | + display: -webkit-box; |
| 673 | + -webkit-line-clamp: 2; |
| 674 | + -webkit-box-orient: vertical; |
| 675 | + overflow-wrap: anywhere; |
| 676 | +} |
| 677 | +.sidebar-outline-topic small { |
| 678 | + flex-shrink: 0; |
| 679 | +} |
| 680 | +.sidebar-outline-topic:hover { |
| 681 | + background: var(--chrome-pressed); |
| 682 | + color: var(--text); |
| 683 | +} |
619 | 684 | .sidebar-outline-item.sidebar-outline-read { |
620 | 685 | color: var(--text-secondary); |
621 | 686 | } |
|
0 commit comments