bookish-press 0.5.2 → 0.5.4

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 (57) hide show
  1. package/dist/components/BookPreview.svelte +1 -0
  2. package/dist/components/chapter/Callout.svelte +0 -1
  3. package/dist/components/chapter/Embed.svelte +8 -6
  4. package/dist/components/chapter/ExtendedPrism.d.ts +1 -1
  5. package/dist/components/chapter/Figure.svelte +2 -1
  6. package/dist/components/chapter/Footnote.svelte +1 -0
  7. package/dist/components/chapter/List.svelte +16 -33
  8. package/dist/components/chapter/Marginal.svelte +4 -1
  9. package/dist/components/chapter/Paragraph.svelte +7 -2
  10. package/dist/components/chapter/Quote.svelte +6 -6
  11. package/dist/components/chapter/Table.svelte +4 -0
  12. package/dist/components/controls/DarkToggle.svelte +1 -1
  13. package/dist/components/editor/BookishEditor.svelte +21 -26
  14. package/dist/components/editor/Commands.js +14 -14
  15. package/dist/components/editor/ConfirmButton.svelte +8 -2
  16. package/dist/components/editor/EmbedEditor.svelte +3 -3
  17. package/dist/components/editor/ImageChooser.svelte +8 -27
  18. package/dist/components/editor/ImageThumbnail.svelte +65 -0
  19. package/dist/components/editor/ImageThumbnail.svelte.d.ts +19 -0
  20. package/dist/components/editor/Permissions.svelte +2 -0
  21. package/dist/components/editor/TextEditor.svelte +5 -2
  22. package/dist/components/editor/Toggle.svelte +1 -1
  23. package/dist/components/editor/Toolbar.svelte +2 -1
  24. package/dist/components/page/Chapter.svelte +1 -1
  25. package/dist/components/page/DefinitionView.svelte +43 -38
  26. package/dist/components/page/Edition.svelte +41 -68
  27. package/dist/components/page/EditionLoader.svelte +12 -3
  28. package/dist/components/page/Editions.svelte +1 -1
  29. package/dist/components/page/Header.svelte +73 -69
  30. package/dist/components/page/Media.svelte +5 -4
  31. package/dist/components/page/MediaPreview.svelte +8 -2
  32. package/dist/components/page/MediaPreview.svelte.d.ts +1 -0
  33. package/dist/components/page/Outline.svelte +1 -1
  34. package/dist/components/page/Page.svelte +6 -3
  35. package/dist/components/page/Reference.svelte +1 -1
  36. package/dist/components/page/Search.svelte +1 -1
  37. package/dist/components/page/TableOfContents.svelte +3 -6
  38. package/dist/components/page/TableOfContentsRow.svelte +1 -1
  39. package/dist/components/page/ThemeEditor.svelte +1 -1
  40. package/dist/models/CRUD.d.ts +2 -1
  41. package/dist/models/CRUD.js +18 -5
  42. package/dist/models/book/BookMedia.d.ts +3 -0
  43. package/dist/models/book/BookMedia.js +22 -5
  44. package/dist/models/book/Chapter.d.ts +9 -7
  45. package/dist/models/book/Chapter.js +16 -6
  46. package/dist/models/book/Edition.d.ts +6 -1
  47. package/dist/models/book/Edition.js +22 -10
  48. package/dist/models/book/Words.js +0 -1
  49. package/dist/models/chapter/ChapterNode.d.ts +1 -0
  50. package/dist/models/chapter/ChapterNode.js +3 -0
  51. package/dist/models/chapter/DefinitionNode.js +1 -1
  52. package/dist/models/chapter/FormatNode.js +4 -1
  53. package/dist/models/chapter/InlineCodeNode.js +1 -1
  54. package/dist/models/chapter/LinkNode.js +1 -1
  55. package/dist/models/chapter/ListNode.js +1 -1
  56. package/dist/models/chapter/Parser.js +3 -6
  57. package/package.json +25 -23
@@ -361,7 +361,7 @@ let editable = isBookEditable() ||
361
361
  {/if}
362
362
 
363
363
  {#if editable || isEditionPartiallyEditable()}
364
- <PageHeader>Editors</PageHeader>
364
+ <PageHeader id="editors">Editors</PageHeader>
365
365
  <Instructions {editable}
366
366
  >These authors can edit this chapter. Add an email here to
367
367
  provide chapter-level edit permissions.</Instructions
@@ -5,13 +5,13 @@ import BookishEditor from '../editor/BookishEditor.svelte';
5
5
  import Format from '../chapter/Format.svelte';
6
6
  import Note from '../editor/Note.svelte';
7
7
  import { afterUpdate } from 'svelte';
8
- import { getUser, getEdition, getLeasee, isChapterEditable, lease, } from './Contexts';
8
+ import { getUser, getEdition, getLeasee, isChapterEditable, lease, isEditionEditable, } from './Contexts';
9
9
  import Button from '../app/Button.svelte';
10
10
  export let id;
11
11
  export let definition;
12
12
  let auth = getUser();
13
13
  let edition = getEdition();
14
- let editable = isChapterEditable();
14
+ let editable = isEditionEditable();
15
15
  // Focus after adding a new synonym.
16
16
  let newSynonym = false;
17
17
  let synonymsEditor = null;
@@ -36,6 +36,23 @@ function addSynonym() {
36
36
  }
37
37
  newSynonym = true;
38
38
  }
39
+ function removeSynonym(index) {
40
+ if (definition.synonyms &&
41
+ index >= 0 &&
42
+ index < definition.synonyms.length)
43
+ updateSynonyms([
44
+ ...definition.synonyms.slice(0, index),
45
+ ...definition.synonyms.slice(index + 1),
46
+ ]);
47
+ }
48
+ function updateSynonyms(synonyms) {
49
+ if ($edition)
50
+ edition.set($edition.withEditedDefinition(id, {
51
+ phrase: definition.phrase,
52
+ definition: definition.definition,
53
+ synonyms: synonyms,
54
+ }));
55
+ }
39
56
  $: syns = definition.synonyms || [];
40
57
  </script>
41
58
 
@@ -67,11 +84,11 @@ $: syns = definition.synonyms || [];
67
84
  {#if editable}
68
85
  <ConfirmButton
69
86
  tooltip="delete definition of {definition.phrase}"
70
- confirm="remove definition?"
87
+ confirm="delete entry"
71
88
  command={() =>
72
89
  $edition
73
90
  ? edition.set($edition.withoutDefinition(id))
74
- : undefined}>- definition</ConfirmButton
91
+ : undefined}>⨉</ConfirmButton
75
92
  >
76
93
  {/if}
77
94
  </dt>
@@ -111,40 +128,26 @@ $: syns = definition.synonyms || [];
111
128
  {/if}
112
129
  {#if edition && editable}
113
130
  <span bind:this={synonymsEditor}>
114
- {#if syns.length === 0}
115
- <Note>no synonyms</Note>
116
- {:else}
117
- {#each syns as syn, index}
118
- <Note>
119
- <TextEditor
120
- text={syn}
121
- label={'Synonym editor.'}
122
- placeholder="synonym"
123
- valid={() => undefined}
124
- save={(text) => {
125
- const newSyns = [...syns];
126
- // Remove the synonym if it's empty but wasn't before
127
- if (syn.length > 0 && text.length === 0)
128
- newSyns.splice(index, 1);
129
- else newSyns[index] = text;
130
- if ($edition)
131
- edition.set(
132
- $edition.withEditedDefinition(
133
- id,
134
- {
135
- phrase: definition.phrase,
136
- definition:
137
- definition.definition,
138
- synonyms: newSyns,
139
- }
140
- )
141
- );
142
- }}
143
- />
144
- </Note>
145
- {#if syns.length > 1 && index < syns.length - 1},&nbsp;{/if}
146
- {/each}
147
- {/if}
131
+ {#each syns as syn, index}
132
+ <Note>
133
+ <TextEditor
134
+ text={syn}
135
+ label={'Synonym editor.'}
136
+ placeholder="synonym"
137
+ valid={() => undefined}
138
+ save={(text) => {
139
+ const newSyns = [...syns];
140
+ newSyns[index] = text;
141
+ updateSynonyms(newSyns);
142
+ }}
143
+ />
144
+ <Button
145
+ tooltip="delete synonym"
146
+ command={() => removeSynonym(index)}>x</Button
147
+ >
148
+ </Note>
149
+ {#if syns.length > 1 && index < syns.length - 1},&nbsp;{/if}
150
+ {/each}
148
151
  </span>&nbsp;<Button tooltip="add synonym" command={addSynonym}
149
152
  >+ synonym</Button
150
153
  >
@@ -162,6 +165,8 @@ $: syns = definition.synonyms || [];
162
165
  }
163
166
 
164
167
  dt {
168
+ font-family: var(--bookish-header-font-family);
165
169
  font-weight: bold;
170
+ vertical-align: baseline;
166
171
  }
167
172
  </style>
@@ -2,9 +2,10 @@
2
2
  import { goto } from '$app/navigation';
3
3
  import { onMount, setContext, tick } from 'svelte';
4
4
  import { writable } from 'svelte/store';
5
- import { BASE, DARK_MODE, } from './Contexts';
5
+ import { BASE, DARK_MODE, getBook, } from './Contexts';
6
6
  import { BookishTheme } from '../../models/book/Theme';
7
7
  import { isDark, setDark } from '../../util/dark';
8
+ import { page } from '$app/stores';
8
9
  // Poly fill smooth scrolling for Safari.
9
10
  onMount(() => smoothscroll.polyfill());
10
11
  // The book edition to render.
@@ -14,6 +15,7 @@ export let edition;
14
15
  // but when the book is being viewed or edited in the Bookish app, it needs a prefix for the
15
16
  // route in the app.
16
17
  export let base = '';
18
+ let book = getBook();
17
19
  // When the base changes, update the context.
18
20
  let baseStore = writable(base);
19
21
  $: setContext(BASE, baseStore);
@@ -29,6 +31,8 @@ $: {
29
31
  // Set the theme, whatever it is, and change it when the edition changes
30
32
  setTheme(edition.getTheme() ?? BookishTheme);
31
33
  }
34
+ // The CSS to set on the edition.
35
+ let themeCSS;
32
36
  // Set the theme on mount.
33
37
  onMount(() => {
34
38
  setTheme(edition.getTheme() ?? BookishTheme);
@@ -36,74 +40,25 @@ onMount(() => {
36
40
  // Redirect old hash routes by simply replacing their hash before routing.
37
41
  if (typeof window !== 'undefined' && window.location.hash.startsWith('#/'))
38
42
  goto(location.hash.replace('#', ''));
39
- /** Given a theme, sets the appropriate CSS rules in the browser to apply the theme.*/
43
+ /** Given a theme, sets the appropriate CSS rules in the browser to apply the theme. */
40
44
  function setTheme(theme) {
41
- // Allow for prerendering; rely on mount callback to set the theme.
42
- if (typeof document === 'undefined')
43
- return;
44
- // If the theme is being unset, make sure we've removed any overrding style declaration.
45
- // This let's the default theme kick in.
46
- let themeTagImports = document.getElementById('bookish-theme-imports');
47
- let themeTagCSS = document.getElementById('bookish-theme');
48
- // We only want to set it if it's actually different than what's currently set.
49
- // Otherwise during editing we get lots of jittery changes on each edit, because the
50
- // whole edition is rerendering. To check this, we set a data attribute on the style tag containing
51
- // a stringify of the theme set. We do this separately for the imports, since they can have more
52
- // major changes than the spacing and color sizes, changing fonts especially.
53
- const themeTagCSSValue = themeTagCSS?.dataset.css;
54
- const themeTagImportsValue = themeTagImports?.dataset.imports;
55
- const newThemeTagCSSValue = JSON.stringify(theme);
56
- const newThemeTagImportsValue = JSON.stringify(theme?.imports);
57
- if (theme !== null) {
58
- // If the imports changed, update theme.
59
- if (themeTagImportsValue !== newThemeTagImportsValue) {
60
- // If it's being set, create a new style tag.
61
- const newThemeImportsTag = document.createElement('style');
62
- // Give it an ID so we can remove it later.
63
- newThemeImportsTag.setAttribute('id', 'bookish-theme-imports');
64
- newThemeImportsTag.dataset.imports = newThemeTagImportsValue;
65
- // Insert any import statements, then any rules.
66
- const css = `${(theme.imports ?? [])
67
- .map((url) => `@import url(${url});`)
68
- .join('\n')}`;
69
- newThemeImportsTag.appendChild(document.createTextNode(css));
70
- document.head.appendChild(newThemeImportsTag);
71
- // Remove the old tag
72
- themeTagImports?.remove();
73
- }
74
- // If the rules changed, update them.
75
- if (themeTagCSSValue !== newThemeTagCSSValue) {
76
- // If it's being set, create a new style tag.
77
- const newThemeCSSTag = document.createElement('style');
78
- // Give it an ID so we can remove it later.
79
- newThemeCSSTag.setAttribute('id', 'bookish-theme');
80
- newThemeCSSTag.dataset.css = newThemeTagCSSValue;
81
- // Insert any import statements, then any rules.
82
- const css = `
83
- ${(theme.imports ?? [])
84
- .map((url) => `@import url(${url});`)
85
- .join('\n')}
86
- .bookish {
87
- ${theme.light ? toRules(theme.light) : ''}
88
- ${theme.fonts ? toRules(theme.fonts) : ''}
89
- ${theme.sizes ? toRules(theme.sizes) : ''}
90
- ${theme.weights ? toRules(theme.weights) : ''}
91
- ${theme.spacing ? toRules(theme.spacing) : ''}
92
- }
93
- .dark {
94
- ${theme.dark ? toRules(theme.dark) : ''}
95
- }`;
96
- newThemeCSSTag.appendChild(document.createTextNode(css));
97
- document.head.appendChild(newThemeCSSTag);
98
- // Remove the old tag
99
- themeTagCSS?.remove();
100
- }
101
- }
102
- else {
103
- // Remove the old tags, unsetting the theme.
104
- themeTagImports?.remove();
105
- themeTagCSS?.remove();
106
- }
45
+ if (theme === null)
46
+ theme = BookishTheme;
47
+ themeCSS = `
48
+ ${(theme.imports ?? [])
49
+ .map((url) => `@import url(${url});`)
50
+ .join('\n')}
51
+ .bookish {
52
+ ${theme.light ? toRules(theme.light) : ''}
53
+ ${theme.fonts ? toRules(theme.fonts) : ''}
54
+ ${theme.sizes ? toRules(theme.sizes) : ''}
55
+ ${theme.weights ? toRules(theme.weights) : ''}
56
+ ${theme.spacing ? toRules(theme.spacing) : ''}
57
+ }
58
+ .dark {
59
+ ${theme.dark ? toRules(theme.dark) : ''}
60
+ }
61
+ `;
107
62
  }
108
63
  function toRules(set) {
109
64
  return Object.keys(set)
@@ -124,6 +79,24 @@ function toRules(set) {
124
79
  }
125
80
  </script>
126
81
 
82
+ <!-- Keep the imports up to date -->
83
+ <svelte:head>
84
+ {@html '<' + `style>${themeCSS}</style>`}
85
+
86
+ <meta property="og:title" content={edition.getTitle()} />
87
+ <meta property="og:image" content={edition.getImage('cover')} />
88
+ <meta property="og:description" content={edition.getDescription()} />
89
+ <meta property="og:type" content="book" />
90
+ <meta property="og:author" content={edition.getAuthorsText()} />
91
+ <meta
92
+ property="og:url"
93
+ content={edition.base ??
94
+ `https://bookish.press/${
95
+ $book && $book.domain ? $book.domain : $page.params.bookid
96
+ }`}
97
+ />
98
+ </svelte:head>
99
+
127
100
  <main class="bookish {$darkMode ? ' dark' : ''}">
128
101
  <slot />
129
102
  </main>
@@ -4,7 +4,7 @@ import Feedback from '../app/Feedback.svelte';
4
4
  import { getBook, getEdition, getUser, getChapterText, } from './Contexts';
5
5
  import { onDestroy } from 'svelte';
6
6
  import { page } from '$app/stores';
7
- import { listenToChapters, listenToEdition } from '../../models/CRUD';
7
+ import { deleteChapterText, listenToChapters, listenToEdition, } from '../../models/CRUD';
8
8
  export let write;
9
9
  let auth = getUser();
10
10
  let book = getBook();
@@ -67,8 +67,17 @@ function listen() {
67
67
  // Update the chapter text store.
68
68
  chapterText.set(chapters);
69
69
  // Update the chapter text in the current edition.
70
- if ($edition)
71
- edition.set($edition.withChapterText(chapters));
70
+ if ($edition) {
71
+ const newEdition = $edition.withChapterText(chapters);
72
+ if (newEdition !== $edition)
73
+ edition.set(newEdition);
74
+ // Get all the chapters in the updated chapter list and
75
+ // delete the ones not referenced by the edition.
76
+ chapters
77
+ .filter(([ref]) => $edition &&
78
+ !$edition.hasChapterRefID(ref.id))
79
+ .forEach(([ref]) => deleteChapterText(ref));
80
+ }
72
81
  });
73
82
  }
74
83
  }
@@ -84,7 +84,7 @@ async function handleDraftEdition() {
84
84
  undefined,
85
85
  edition.summary
86
86
  )}
87
- />{:else}&mdash{/if}</p
87
+ />{:else}&mdash;{/if}</p
88
88
  >
89
89
  </section>
90
90
  {/if}
@@ -54,80 +54,84 @@ let embed;
54
54
  $: embed = getImage();
55
55
  </script>
56
56
 
57
- <header class="bookish-chapter-header">
58
- {#if embed}
59
- <div class="bookish-figure-full">
60
- {#if $edition && editable}
61
- <BookishEditor
62
- text={embed}
63
- parser={(text) => Parser.parseEmbed($edition, text)}
64
- save={(node) =>
65
- setImage ? setImage(node.toBookdown()) : undefined}
66
- chapter={false}
67
- component={Embed}
68
- placeholder=""
69
- leasee={getLeasee(auth, edition, `embed-${id}`)}
70
- lease={(lock) => lease(auth, edition, `embed-${id}`, lock)}
71
- />
72
- {:else}
73
- <Embed
74
- node={Parser.parseEmbed($edition, embed)}
75
- editable={false}
76
- />
77
- {/if}
78
- {#if !print && showReminder}
79
- <div class="bookish-scroll-reminder" />
80
- {/if}
81
- </div>
82
- {:else}
83
- <!-- Add a bit of space to account for the lack of an image. -->
84
- <p>&nbsp;</p>
85
- {/if}
86
- {#if !print}
87
- <slot name="outline" />
88
- {/if}
89
- {#if editable}
90
- {#if embed === null}
91
- <Button tooltip="add cover image" command={addCover}
92
- >+ cover image</Button
93
- >
57
+ <!-- We key on the chapter ID to avoid laggy updates from image loading -->
58
+ {#key id}
59
+ <header class="bookish-chapter-header">
60
+ {#if embed}
61
+ <div class="bookish-figure-full">
62
+ {#if $edition && editable}
63
+ <BookishEditor
64
+ text={embed}
65
+ parser={(text) => Parser.parseEmbed($edition, text)}
66
+ save={(node) =>
67
+ setImage ? setImage(node.toBookdown()) : undefined}
68
+ chapter={false}
69
+ component={Embed}
70
+ placeholder=""
71
+ leasee={getLeasee(auth, edition, `embed-${id}`)}
72
+ lease={(lock) =>
73
+ lease(auth, edition, `embed-${id}`, lock)}
74
+ />
75
+ {:else}
76
+ <Embed
77
+ node={Parser.parseEmbed($edition, embed)}
78
+ editable={false}
79
+ />
80
+ {/if}
81
+ {#if !print && showReminder}
82
+ <div class="bookish-scroll-reminder" />
83
+ {/if}
84
+ </div>
94
85
  {:else}
95
- <ConfirmButton
96
- confirm="remove image?"
97
- tooltip="remove cover image"
98
- command={removeCover}>- cover image</ConfirmButton
99
- >
86
+ <!-- Add a bit of space to account for the lack of an image. -->
87
+ <p>&nbsp;</p>
100
88
  {/if}
101
- {/if}
102
- <div bind:this={title} class="bookish-chapter-header-text">
103
- <slot name="before" />
104
- <Title>
105
- {#if editable && save}
106
- <TextEditor
107
- {label}
108
- text={header + (subtitle ? ': ' + subtitle : '')}
109
- placeholder="title"
110
- valid={(text) =>
111
- text.length === 0
112
- ? 'Titles have to be at least one character long.'
113
- : undefined}
114
- {save}
115
- />
89
+ {#if !print}
90
+ <slot name="outline" />
91
+ {/if}
92
+ {#if editable}
93
+ {#if embed === null}
94
+ <Button tooltip="add cover image" command={addCover}
95
+ >+ cover image</Button
96
+ >
116
97
  {:else}
117
- {header}
118
- {#if subtitle}<div class="bookish-subtitle">{subtitle}</div
119
- >{/if}
98
+ <ConfirmButton
99
+ confirm="delete image"
100
+ tooltip="delete cover image"
101
+ command={removeCover}>- cover image</ConfirmButton
102
+ >
120
103
  {/if}
121
- </Title>
122
- <slot name="after" />
123
- {#if tags}
124
- <div
125
- >{#each tags as tag}<span class="bookish-tag">{tag}</span
126
- >{/each}</div
127
- >
128
104
  {/if}
129
- </div>
130
- </header>
105
+ <div bind:this={title} class="bookish-chapter-header-text">
106
+ <slot name="before" />
107
+ <Title>
108
+ {#if editable && save}
109
+ <TextEditor
110
+ {label}
111
+ text={header + (subtitle ? ': ' + subtitle : '')}
112
+ placeholder="title"
113
+ valid={(text) =>
114
+ text.length === 0
115
+ ? 'Titles have to be at least one character long.'
116
+ : undefined}
117
+ {save}
118
+ />
119
+ {:else}
120
+ {header}
121
+ {#if subtitle}<div class="bookish-subtitle">{subtitle}</div
122
+ >{/if}
123
+ {/if}
124
+ </Title>
125
+ <slot name="after" />
126
+ {#if tags}
127
+ <div
128
+ >{#each tags as tag}<span class="bookish-tag">{tag}</span
129
+ >{/each}</div
130
+ >
131
+ {/if}
132
+ </div>
133
+ </header>
134
+ {/key}
131
135
 
132
136
  <style>
133
137
  .bookish-subtitle {
@@ -24,7 +24,7 @@ onMount(() => {
24
24
  media?.notify(updateImages);
25
25
  return () => media?.stopNotifying(updateImages);
26
26
  });
27
- $: unused = images?.filter((image) => embeds.find((embed) => embed.getURL() === image.url) === undefined);
27
+ $: unused = images?.filter((image) => !embeds.some((embed) => image.url.includes(embed.embed.getURL())));
28
28
  </script>
29
29
 
30
30
  {#if $edition}
@@ -59,9 +59,10 @@ $: unused = images?.filter((image) => embeds.find((embed) => embed.getURL() ===
59
59
  {:else}
60
60
  <p>These are the images in the book:</p>
61
61
  {/if}
62
- {#each embeds as embed}
62
+ {#each embeds as { embed, chapterID }}
63
63
  {#if !embed.isVideo()}
64
64
  <MediaPreview
65
+ {chapterID}
65
66
  url={embed.isLocal()
66
67
  ? `${$base}/${embed.getSmallURL()}`
67
68
  : embed.getSmallURL()}
@@ -89,10 +90,10 @@ $: unused = images?.filter((image) => embeds.find((embed) => embed.getURL() ===
89
90
  be used in other editions of this book.
90
91
  </Instructions>
91
92
  {#each unused as image}
92
- <MediaPreview url={image.url} alt={''}>
93
+ <MediaPreview chapterID={undefined} url={image.url} alt={''}>
93
94
  <ConfirmButton
94
95
  tooltip="delete unused image"
95
- confirm="delete image?"
96
+ confirm="delete image"
96
97
  command={() =>
97
98
  media
98
99
  ?.remove(image)
@@ -1,5 +1,9 @@
1
- <script>export let url;
1
+ <script>import Link from '../Link.svelte';
2
+ import { getBase } from './Contexts';
3
+ export let url;
2
4
  export let alt;
5
+ export let chapterID;
6
+ let base = getBase();
3
7
  let loaded;
4
8
  let urlChecked;
5
9
  $: {
@@ -18,7 +22,9 @@ $: {
18
22
 
19
23
  <figure class={'media-preview'}>
20
24
  {#if loaded}
21
- <img src={url.startsWith('http') ? url : `${url}`} {alt} />
25
+ <Link to={`${$base}/${chapterID}`} linked={chapterID !== undefined}
26
+ ><img src={url.startsWith('http') ? url : `${url}`} {alt} /></Link
27
+ >
22
28
  {:else if loaded === null}
23
29
  <div class="missing">missing image</div>
24
30
  {/if}
@@ -3,6 +3,7 @@ declare const __propDef: {
3
3
  props: {
4
4
  url: string;
5
5
  alt: string;
6
+ chapterID: string | undefined;
6
7
  };
7
8
  events: {
8
9
  [evt: string]: CustomEvent<any>;
@@ -129,7 +129,7 @@ let expandLabel = '\u2630';
129
129
  aria-label={expanded
130
130
  ? 'Collapse navigation menu'
131
131
  : 'Expand navigation menu'}
132
- tabIndex="0"
132
+ tabindex="0"
133
133
  on:click={headers.length > 0 ? toggleExpanded : undefined}
134
134
  on:keydown={(event) => {
135
135
  if (event.key === 'Enter' || event.key === ' ')
@@ -2,10 +2,12 @@
2
2
  import Loading from './Loading.svelte';
3
3
  export let title;
4
4
  export let afterLoaded = undefined;
5
- let loaded = false;
5
+ let fontsLoaded = false;
6
+ let imagesLoaded = false;
6
7
  let lastHeight = 0;
7
8
  let intervalID = null;
8
9
  let mountTime = Date.now();
10
+ $: loaded = fontsLoaded && imagesLoaded;
9
11
  function watchLoading() {
10
12
  const bodyHeight = document.body.clientHeight;
11
13
  // If the height hasn't changed and images are loaded OR it's been 2 seconds, load.
@@ -14,7 +16,7 @@ function watchLoading() {
14
16
  // Stop watching the images
15
17
  stopWatching();
16
18
  // Set to loaded to hide the overlay and notify callback if there is one.
17
- loaded = true;
19
+ imagesLoaded = true;
18
20
  if (afterLoaded)
19
21
  afterLoaded();
20
22
  }
@@ -29,6 +31,7 @@ onMount(() => {
29
31
  // Watch the height of the document and wait until it's been stable for a while before scrolling
30
32
  // to any targets. We use the data below to monitor the document height over time.
31
33
  intervalID = setInterval(() => watchLoading(), 50);
34
+ document.fonts.ready.then(() => (fontsLoaded = true));
32
35
  return () => stopWatching();
33
36
  });
34
37
  function imagesAreLoaded() {
@@ -45,7 +48,7 @@ function imagesAreLoaded() {
45
48
  <meta name="description" content={title} />
46
49
  </svelte:head>
47
50
 
48
- <section class={'bookish-page' + (loaded ? ' loaded' : '')}>
51
+ <section class={'bookish-page' + (imagesLoaded ? ' loaded' : '')}>
49
52
  <slot />
50
53
  </section>
51
54
  <!-- Overlay loading feedback if loading, but still render the page. -->
@@ -249,7 +249,7 @@ function move(field, direction) {
249
249
  {#if edit && editable && !reference.short}
250
250
  <ConfirmButton
251
251
  tooltip="delete reference {reference.title}"
252
- confirm="delete reference..."
252
+ confirm="delete reference"
253
253
  command={() =>
254
254
  $edition
255
255
  ? edition.set($edition.withoutReference(reference.citationID))
@@ -83,7 +83,7 @@ $: {
83
83
 
84
84
  <p>
85
85
  Type a word—just a single word—and we'll show its occurrences in
86
- this book:
86
+ this book's chapters:
87
87
  </p>
88
88
 
89
89
  <input
@@ -138,7 +138,7 @@ onMount(() => {
138
138
  </Instructions>
139
139
 
140
140
  <PageHeader id="chapters"
141
- >Chapters {#if editable}<Button
141
+ >Chapters{#if editable}&nbsp;<Button
142
142
  tooltip="add empty chapter"
143
143
  disabled={waitingForChapter}
144
144
  command={addChapter}>+ chapter</Button
@@ -273,10 +273,7 @@ onMount(() => {
273
273
 
274
274
  <!-- Book citation -->
275
275
  <PageParagraph>
276
- {$edition
277
- .getAuthors()
278
- .map((author) => Parser.parseFormat($edition, author).toText())
279
- .join(', ')} ({new Date().getFullYear()}).
276
+ {$edition.getAuthorsText()} ({new Date().getFullYear()}).
280
277
  <em>{$edition.getTitle()}</em>. {bookURL},
281
278
  <em>retrieved {new Date().toLocaleDateString('en-US')}</em>.
282
279
  </PageParagraph>
@@ -292,7 +289,7 @@ onMount(() => {
292
289
  {/if}
293
290
 
294
291
  {#if $book && (editable || isEditionPartiallyEditable())}
295
- <PageHeader>Editors</PageHeader>
292
+ <PageHeader id="editors">Editors</PageHeader>
296
293
  <Instructions {editable}>
297
294
  These authors can edit this edition and its chapters. To give
298
295
  book-level permissions, edit the book editors on the <Link
@@ -76,7 +76,7 @@ $: chapterURL =
76
76
  >
77
77
  <ConfirmButton
78
78
  tooltip="delete chapter {title}"
79
- confirm="delete?"
79
+ confirm="delete"
80
80
  command={() =>
81
81
  chapter && $edition
82
82
  ? edition.set(