docbook 0.1.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 (271) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +5 -0
  3. data/CLAUDE.md +19 -0
  4. data/CODE_OF_CONDUCT.md +10 -0
  5. data/README.adoc +335 -0
  6. data/Rakefile +12 -0
  7. data/docs/.lycheeignore +33 -0
  8. data/docs/Gemfile +10 -0
  9. data/docs/INDEX.adoc +67 -0
  10. data/docs/_config.yml +186 -0
  11. data/docs/advanced/element-classes.adoc +185 -0
  12. data/docs/advanced/frontend-customization.adoc +193 -0
  13. data/docs/advanced/index.adoc +14 -0
  14. data/docs/advanced/templates.adoc +123 -0
  15. data/docs/features/element-coverage.adoc +373 -0
  16. data/docs/features/html-output/data-model.adoc +285 -0
  17. data/docs/features/html-output/directory-mode.adoc +180 -0
  18. data/docs/features/html-output/index.adoc +90 -0
  19. data/docs/features/html-output/single-file-mode.adoc +125 -0
  20. data/docs/features/index-generation.adoc +197 -0
  21. data/docs/features/index.adoc +63 -0
  22. data/docs/features/numbering.adoc +183 -0
  23. data/docs/features/toc-generation.adoc +150 -0
  24. data/docs/features/xinclude/fragid-schemes.adoc +287 -0
  25. data/docs/features/xinclude/index.adoc +119 -0
  26. data/docs/features/xinclude/text-includes.adoc +123 -0
  27. data/docs/features/xinclude/xml-includes.adoc +167 -0
  28. data/docs/getting-started/index.adoc +50 -0
  29. data/docs/getting-started/installation.adoc +113 -0
  30. data/docs/getting-started/quick-start.adoc +161 -0
  31. data/docs/guides/converting-article.adoc +188 -0
  32. data/docs/guides/converting-book.adoc +192 -0
  33. data/docs/guides/index.adoc +12 -0
  34. data/docs/guides/roundtrip-testing.adoc +129 -0
  35. data/docs/interfaces/cli/format-command.adoc +109 -0
  36. data/docs/interfaces/cli/index.adoc +73 -0
  37. data/docs/interfaces/cli/roundtrip-command.adoc +125 -0
  38. data/docs/interfaces/cli/to-html-command.adoc +178 -0
  39. data/docs/interfaces/cli/validate-command.adoc +104 -0
  40. data/docs/interfaces/index.adoc +101 -0
  41. data/docs/interfaces/ruby-api/html-output.adoc +186 -0
  42. data/docs/interfaces/ruby-api/index.adoc +111 -0
  43. data/docs/interfaces/ruby-api/parsing.adoc +202 -0
  44. data/docs/interfaces/ruby-api/xinclude.adoc +162 -0
  45. data/docs/interfaces/ruby-api/xref-resolution.adoc +156 -0
  46. data/docs/lychee.toml +42 -0
  47. data/docs/reference/cli-options.adoc +155 -0
  48. data/docs/reference/content-block-types.adoc +243 -0
  49. data/docs/reference/glossary.adoc +119 -0
  50. data/docs/reference/index.adoc +12 -0
  51. data/docs/reference/supported-elements.adoc +749 -0
  52. data/docs/understanding/architecture.adoc +145 -0
  53. data/docs/understanding/content-pipeline.adoc +102 -0
  54. data/docs/understanding/data-models.adoc +156 -0
  55. data/docs/understanding/index.adoc +34 -0
  56. data/exe/docbook +7 -0
  57. data/frontend/dist/app.css +1 -0
  58. data/frontend/dist/app.iife.js +24 -0
  59. data/frontend/package-lock.json +1445 -0
  60. data/frontend/package.json +22 -0
  61. data/frontend/src/App.vue +230 -0
  62. data/frontend/src/app.ts +8 -0
  63. data/frontend/src/components/AppSidebar.vue +116 -0
  64. data/frontend/src/components/AppendixSection.vue +39 -0
  65. data/frontend/src/components/BlockRenderer.vue +358 -0
  66. data/frontend/src/components/ChapterSection.vue +32 -0
  67. data/frontend/src/components/ContentRenderer.vue +13 -0
  68. data/frontend/src/components/EbookContainer.vue +147 -0
  69. data/frontend/src/components/EbookTopBar.vue +116 -0
  70. data/frontend/src/components/PartSection.vue +44 -0
  71. data/frontend/src/components/ReferenceEntry.vue +80 -0
  72. data/frontend/src/components/SearchModal.vue +286 -0
  73. data/frontend/src/components/SectionContent.vue +31 -0
  74. data/frontend/src/components/SettingsPanel.vue +236 -0
  75. data/frontend/src/components/TocTreeItem.vue +135 -0
  76. data/frontend/src/composables/useEbookStore.ts +191 -0
  77. data/frontend/src/composables/useSearch.ts +249 -0
  78. data/frontend/src/env.d.ts +7 -0
  79. data/frontend/src/stores/documentStore.ts +221 -0
  80. data/frontend/src/stores/uiStore.ts +98 -0
  81. data/frontend/src/styles.css +253 -0
  82. data/frontend/tsconfig.json +24 -0
  83. data/frontend/tsconfig.node.json +11 -0
  84. data/frontend/vite.config.ts +30 -0
  85. data/lib/docbook/cli.rb +123 -0
  86. data/lib/docbook/document.rb +67 -0
  87. data/lib/docbook/elements/abbrev.rb +16 -0
  88. data/lib/docbook/elements/acknowledgements.rb +22 -0
  89. data/lib/docbook/elements/address.rb +18 -0
  90. data/lib/docbook/elements/alt.rb +16 -0
  91. data/lib/docbook/elements/annotation.rb +18 -0
  92. data/lib/docbook/elements/appendix.rb +34 -0
  93. data/lib/docbook/elements/article.rb +31 -0
  94. data/lib/docbook/elements/att.rb +15 -0
  95. data/lib/docbook/elements/attribution.rb +20 -0
  96. data/lib/docbook/elements/audioobject.rb +18 -0
  97. data/lib/docbook/elements/author.rb +18 -0
  98. data/lib/docbook/elements/bibliography.rb +24 -0
  99. data/lib/docbook/elements/bibliomixed.rb +40 -0
  100. data/lib/docbook/elements/biblioref.rb +20 -0
  101. data/lib/docbook/elements/blockquote.rb +26 -0
  102. data/lib/docbook/elements/book.rb +36 -0
  103. data/lib/docbook/elements/buildtarget.rb +16 -0
  104. data/lib/docbook/elements/callout.rb +22 -0
  105. data/lib/docbook/elements/calloutlist.rb +22 -0
  106. data/lib/docbook/elements/caution.rb +30 -0
  107. data/lib/docbook/elements/chapter.rb +62 -0
  108. data/lib/docbook/elements/citation.rb +16 -0
  109. data/lib/docbook/elements/citerefentry.rb +26 -0
  110. data/lib/docbook/elements/citetitle.rb +20 -0
  111. data/lib/docbook/elements/classname.rb +16 -0
  112. data/lib/docbook/elements/code.rb +16 -0
  113. data/lib/docbook/elements/colophon.rb +22 -0
  114. data/lib/docbook/elements/computeroutput.rb +18 -0
  115. data/lib/docbook/elements/copyright.rb +17 -0
  116. data/lib/docbook/elements/danger.rb +28 -0
  117. data/lib/docbook/elements/date.rb +16 -0
  118. data/lib/docbook/elements/dedication.rb +22 -0
  119. data/lib/docbook/elements/dir.rb +16 -0
  120. data/lib/docbook/elements/emphasis.rb +18 -0
  121. data/lib/docbook/elements/entry.rb +30 -0
  122. data/lib/docbook/elements/entrytbl.rb +22 -0
  123. data/lib/docbook/elements/equation.rb +26 -0
  124. data/lib/docbook/elements/example.rb +30 -0
  125. data/lib/docbook/elements/fieldsynopsis.rb +21 -0
  126. data/lib/docbook/elements/figure.rb +28 -0
  127. data/lib/docbook/elements/filename.rb +16 -0
  128. data/lib/docbook/elements/firstname.rb +16 -0
  129. data/lib/docbook/elements/firstterm.rb +18 -0
  130. data/lib/docbook/elements/footnote.rb +22 -0
  131. data/lib/docbook/elements/footnoteref.rb +21 -0
  132. data/lib/docbook/elements/foreignphrase.rb +18 -0
  133. data/lib/docbook/elements/formalpara.rb +20 -0
  134. data/lib/docbook/elements/function.rb +16 -0
  135. data/lib/docbook/elements/glossary.rb +24 -0
  136. data/lib/docbook/elements/glossdef.rb +18 -0
  137. data/lib/docbook/elements/glossentry.rb +26 -0
  138. data/lib/docbook/elements/glosssee.rb +18 -0
  139. data/lib/docbook/elements/glossseealso.rb +18 -0
  140. data/lib/docbook/elements/glossterm.rb +18 -0
  141. data/lib/docbook/elements/holder.rb +16 -0
  142. data/lib/docbook/elements/honorific.rb +16 -0
  143. data/lib/docbook/elements/imagedata.rb +29 -0
  144. data/lib/docbook/elements/imageobject.rb +22 -0
  145. data/lib/docbook/elements/important.rb +30 -0
  146. data/lib/docbook/elements/index.rb +26 -0
  147. data/lib/docbook/elements/indexdiv.rb +22 -0
  148. data/lib/docbook/elements/indexentry.rb +22 -0
  149. data/lib/docbook/elements/indexterm.rb +34 -0
  150. data/lib/docbook/elements/info.rb +25 -0
  151. data/lib/docbook/elements/informalexample.rb +24 -0
  152. data/lib/docbook/elements/informalfigure.rb +20 -0
  153. data/lib/docbook/elements/inlinemediaobject.rb +22 -0
  154. data/lib/docbook/elements/itemizedlist.rb +21 -0
  155. data/lib/docbook/elements/legalnotice.rb +22 -0
  156. data/lib/docbook/elements/link.rb +26 -0
  157. data/lib/docbook/elements/listitem.rb +32 -0
  158. data/lib/docbook/elements/literal.rb +16 -0
  159. data/lib/docbook/elements/literallayout.rb +22 -0
  160. data/lib/docbook/elements/mediaobject.rb +26 -0
  161. data/lib/docbook/elements/msg.rb +20 -0
  162. data/lib/docbook/elements/msgexplan.rb +22 -0
  163. data/lib/docbook/elements/msgset.rb +22 -0
  164. data/lib/docbook/elements/note.rb +30 -0
  165. data/lib/docbook/elements/orderedlist.rb +23 -0
  166. data/lib/docbook/elements/orgname.rb +16 -0
  167. data/lib/docbook/elements/para.rb +61 -0
  168. data/lib/docbook/elements/paragraph_like.rb +18 -0
  169. data/lib/docbook/elements/parameter.rb +16 -0
  170. data/lib/docbook/elements/part.rb +38 -0
  171. data/lib/docbook/elements/personname.rb +22 -0
  172. data/lib/docbook/elements/phrase.rb +20 -0
  173. data/lib/docbook/elements/preface.rb +58 -0
  174. data/lib/docbook/elements/primary.rb +16 -0
  175. data/lib/docbook/elements/procedure.rb +24 -0
  176. data/lib/docbook/elements/productname.rb +18 -0
  177. data/lib/docbook/elements/productnumber.rb +16 -0
  178. data/lib/docbook/elements/programlisting.rb +28 -0
  179. data/lib/docbook/elements/property.rb +16 -0
  180. data/lib/docbook/elements/pubdate.rb +16 -0
  181. data/lib/docbook/elements/publishername.rb +16 -0
  182. data/lib/docbook/elements/quotation.rb +24 -0
  183. data/lib/docbook/elements/quote.rb +18 -0
  184. data/lib/docbook/elements/refentry.rb +32 -0
  185. data/lib/docbook/elements/refentrytitle.rb +16 -0
  186. data/lib/docbook/elements/reference.rb +26 -0
  187. data/lib/docbook/elements/refmeta.rb +29 -0
  188. data/lib/docbook/elements/refmiscinfo.rb +16 -0
  189. data/lib/docbook/elements/refname.rb +16 -0
  190. data/lib/docbook/elements/refnamediv.rb +22 -0
  191. data/lib/docbook/elements/refpurpose.rb +16 -0
  192. data/lib/docbook/elements/refsect1.rb +22 -0
  193. data/lib/docbook/elements/refsect2.rb +22 -0
  194. data/lib/docbook/elements/refsect3.rb +22 -0
  195. data/lib/docbook/elements/refsection.rb +32 -0
  196. data/lib/docbook/elements/releaseinfo.rb +16 -0
  197. data/lib/docbook/elements/remark.rb +20 -0
  198. data/lib/docbook/elements/replaceable.rb +16 -0
  199. data/lib/docbook/elements/row.rb +15 -0
  200. data/lib/docbook/elements/screen.rb +28 -0
  201. data/lib/docbook/elements/secondary.rb +16 -0
  202. data/lib/docbook/elements/sect1.rb +26 -0
  203. data/lib/docbook/elements/sect2.rb +24 -0
  204. data/lib/docbook/elements/sect3.rb +24 -0
  205. data/lib/docbook/elements/sect4.rb +24 -0
  206. data/lib/docbook/elements/sect5.rb +22 -0
  207. data/lib/docbook/elements/section.rb +66 -0
  208. data/lib/docbook/elements/see.rb +16 -0
  209. data/lib/docbook/elements/seealso.rb +18 -0
  210. data/lib/docbook/elements/set.rb +26 -0
  211. data/lib/docbook/elements/setindex.rb +24 -0
  212. data/lib/docbook/elements/sidebar.rb +22 -0
  213. data/lib/docbook/elements/simplesect.rb +32 -0
  214. data/lib/docbook/elements/step.rb +26 -0
  215. data/lib/docbook/elements/substeps.rb +18 -0
  216. data/lib/docbook/elements/subtitle.rb +16 -0
  217. data/lib/docbook/elements/surname.rb +16 -0
  218. data/lib/docbook/elements/table.rb +24 -0
  219. data/lib/docbook/elements/tag.rb +20 -0
  220. data/lib/docbook/elements/tbody.rb +15 -0
  221. data/lib/docbook/elements/term.rb +37 -0
  222. data/lib/docbook/elements/tertiary.rb +16 -0
  223. data/lib/docbook/elements/textobject.rb +18 -0
  224. data/lib/docbook/elements/tfoot.rb +15 -0
  225. data/lib/docbook/elements/tgroup.rb +21 -0
  226. data/lib/docbook/elements/thead.rb +15 -0
  227. data/lib/docbook/elements/tip.rb +30 -0
  228. data/lib/docbook/elements/title.rb +16 -0
  229. data/lib/docbook/elements/toc.rb +24 -0
  230. data/lib/docbook/elements/tocdiv.rb +22 -0
  231. data/lib/docbook/elements/tocentry.rb +20 -0
  232. data/lib/docbook/elements/topic.rb +26 -0
  233. data/lib/docbook/elements/type.rb +16 -0
  234. data/lib/docbook/elements/uri.rb +16 -0
  235. data/lib/docbook/elements/userinput.rb +18 -0
  236. data/lib/docbook/elements/variable.rb +16 -0
  237. data/lib/docbook/elements/variablelist.rb +19 -0
  238. data/lib/docbook/elements/varlistentry.rb +17 -0
  239. data/lib/docbook/elements/version.rb +16 -0
  240. data/lib/docbook/elements/videoobject.rb +18 -0
  241. data/lib/docbook/elements/warning.rb +30 -0
  242. data/lib/docbook/elements/xref.rb +18 -0
  243. data/lib/docbook/elements/year.rb +16 -0
  244. data/lib/docbook/elements.rb +204 -0
  245. data/lib/docbook/models/document_metadata.rb +30 -0
  246. data/lib/docbook/models/document_root.rb +33 -0
  247. data/lib/docbook/models/index_entry.rb +28 -0
  248. data/lib/docbook/models/reading_position.rb +22 -0
  249. data/lib/docbook/models/section_number.rb +18 -0
  250. data/lib/docbook/models/section_root.rb +29 -0
  251. data/lib/docbook/models/toc_node.rb +24 -0
  252. data/lib/docbook/models.rb +16 -0
  253. data/lib/docbook/output/data.rb +196 -0
  254. data/lib/docbook/output/html.rb +1450 -0
  255. data/lib/docbook/output/index_generator.rb +281 -0
  256. data/lib/docbook/output.rb +8 -0
  257. data/lib/docbook/services/document_builder.rb +258 -0
  258. data/lib/docbook/services/element_to_hash.rb +287 -0
  259. data/lib/docbook/services/index_generator.rb +134 -0
  260. data/lib/docbook/services/numbering_service.rb +186 -0
  261. data/lib/docbook/services/toc_generator.rb +138 -0
  262. data/lib/docbook/services.rb +14 -0
  263. data/lib/docbook/templates/document.html.liquid +20 -0
  264. data/lib/docbook/templates/partials/_head.liquid +39 -0
  265. data/lib/docbook/templates/partials/_scripts.liquid +10 -0
  266. data/lib/docbook/version.rb +5 -0
  267. data/lib/docbook/xinclude_resolver.rb +217 -0
  268. data/lib/docbook/xref_resolver.rb +78 -0
  269. data/lib/docbook.rb +17 -0
  270. data/sig/docbook.rbs +4 -0
  271. metadata +385 -0
@@ -0,0 +1,236 @@
1
+ <template>
2
+ <Teleport to="body">
3
+ <Transition name="modal">
4
+ <div
5
+ v-if="isSettingsOpen"
6
+ class="fixed inset-0 z-50 flex items-center justify-center"
7
+ @click.self="ebookStore.toggleSettings"
8
+ >
9
+ <div class="absolute inset-0 bg-black/50 backdrop-blur-sm" @click="ebookStore.toggleSettings"></div>
10
+
11
+ <div class="relative bg-white dark:bg-gray-900 rounded-xl shadow-2xl w-full max-w-md mx-4 overflow-hidden">
12
+ <!-- Header -->
13
+ <div class="flex items-center justify-between px-6 py-4 border-b border-gray-200 dark:border-gray-700">
14
+ <h2 class="text-lg font-semibold text-gray-900 dark:text-white">Display Settings</h2>
15
+ <button
16
+ @click="ebookStore.toggleSettings"
17
+ class="p-1 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800 text-gray-500"
18
+ >
19
+ <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
20
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
21
+ </svg>
22
+ </button>
23
+ </div>
24
+
25
+ <!-- Content -->
26
+ <div class="px-6 py-5 space-y-6">
27
+ <!-- Reading Mode -->
28
+ <div>
29
+ <label class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-3 block">Reading Mode</label>
30
+ <div class="grid grid-cols-4 gap-2">
31
+ <button
32
+ v-for="mode in readingModes"
33
+ :key="mode.value"
34
+ @click="ebookStore.setReadingMode(mode.value)"
35
+ :class="[
36
+ 'flex flex-col items-center justify-center p-3 rounded-lg border-2 transition-all',
37
+ currentReadingMode === mode.value
38
+ ? 'border-cyan-500 bg-cyan-50 dark:bg-cyan-900/30 text-cyan-700 dark:text-cyan-300'
39
+ : 'border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600 text-gray-600 dark:text-gray-400'
40
+ ]"
41
+ >
42
+ <span class="text-xl mb-1">{{ mode.icon }}</span>
43
+ <span class="text-xs">{{ mode.label }}</span>
44
+ </button>
45
+ </div>
46
+ </div>
47
+
48
+ <!-- Divider -->
49
+ <div class="border-t border-gray-200 dark:border-gray-700"></div>
50
+
51
+ <!-- Theme -->
52
+ <div>
53
+ <label class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-3 block">Theme</label>
54
+ <div class="grid grid-cols-4 gap-2">
55
+ <button
56
+ v-for="t in themes"
57
+ :key="t.value"
58
+ @click="ebookStore.setTheme(t.value)"
59
+ :class="[
60
+ 'flex flex-col items-center justify-center p-3 rounded-lg border-2 transition-all',
61
+ currentTheme === t.value
62
+ ? 'border-cyan-500 bg-cyan-50 dark:bg-cyan-900/30'
63
+ : 'border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600'
64
+ ]"
65
+ >
66
+ <div :class="['w-8 h-8 rounded-full mb-1', t.preview]"></div>
67
+ <span class="text-xs" :class="t.textClass">{{ t.label }}</span>
68
+ </button>
69
+ </div>
70
+ </div>
71
+
72
+ <!-- Divider -->
73
+ <div class="border-t border-gray-200 dark:border-gray-700"></div>
74
+
75
+ <!-- Font Size -->
76
+ <div>
77
+ <div class="flex items-center justify-between mb-2">
78
+ <label class="text-sm font-medium text-gray-700 dark:text-gray-300">Font Size</label>
79
+ <span class="text-sm text-gray-500 dark:text-gray-400">{{ currentFontSize }}px</span>
80
+ </div>
81
+ <input
82
+ type="range"
83
+ min="12"
84
+ max="32"
85
+ :value="currentFontSize"
86
+ @input="ebookStore.setFontSize(Number(($event.target as HTMLInputElement).value))"
87
+ class="w-full h-2 bg-gray-200 dark:bg-gray-700 rounded-lg appearance-none cursor-pointer accent-cyan-500"
88
+ />
89
+ <div class="flex justify-between text-xs text-gray-400 mt-1">
90
+ <span>12px</span>
91
+ <span>32px</span>
92
+ </div>
93
+ </div>
94
+
95
+ <!-- Font Weight -->
96
+ <div>
97
+ <div class="flex items-center justify-between mb-2">
98
+ <label class="text-sm font-medium text-gray-700 dark:text-gray-300">Font Weight</label>
99
+ <span class="text-sm text-gray-500 dark:text-gray-400">{{ currentFontWeight }}</span>
100
+ </div>
101
+ <input
102
+ type="range"
103
+ min="300"
104
+ max="700"
105
+ step="100"
106
+ :value="currentFontWeight"
107
+ @input="ebookStore.setFontWeight(Number(($event.target as HTMLInputElement).value))"
108
+ class="w-full h-2 bg-gray-200 dark:bg-gray-700 rounded-lg appearance-none cursor-pointer accent-cyan-500"
109
+ />
110
+ <div class="flex justify-between text-xs text-gray-400 mt-1">
111
+ <span>Light</span>
112
+ <span>Bold</span>
113
+ </div>
114
+ </div>
115
+
116
+ <!-- Line Height -->
117
+ <div>
118
+ <div class="flex items-center justify-between mb-2">
119
+ <label class="text-sm font-medium text-gray-700 dark:text-gray-300">Line Height</label>
120
+ <span class="text-sm text-gray-500 dark:text-gray-400">{{ currentLineHeight }}</span>
121
+ </div>
122
+ <input
123
+ type="range"
124
+ min="1.2"
125
+ max="2.0"
126
+ step="0.1"
127
+ :value="currentLineHeight"
128
+ @input="ebookStore.setLineHeight(Number(($event.target as HTMLInputElement).value))"
129
+ class="w-full h-2 bg-gray-200 dark:bg-gray-700 rounded-lg appearance-none cursor-pointer accent-cyan-500"
130
+ />
131
+ <div class="flex justify-between text-xs text-gray-400 mt-1">
132
+ <span>Compact</span>
133
+ <span>Spacious</span>
134
+ </div>
135
+ </div>
136
+
137
+ <!-- Margin -->
138
+ <div>
139
+ <div class="flex items-center justify-between mb-2">
140
+ <label class="text-sm font-medium text-gray-700 dark:text-gray-300">Margins</label>
141
+ <span class="text-sm text-gray-500 dark:text-gray-400">{{ currentMargin }}px</span>
142
+ </div>
143
+ <input
144
+ type="range"
145
+ min="16"
146
+ max="96"
147
+ step="8"
148
+ :value="currentMargin"
149
+ @input="ebookStore.setMargin(Number(($event.target as HTMLInputElement).value))"
150
+ class="w-full h-2 bg-gray-200 dark:bg-gray-700 rounded-lg appearance-none cursor-pointer accent-cyan-500"
151
+ />
152
+ <div class="flex justify-between text-xs text-gray-400 mt-1">
153
+ <span>Narrow</span>
154
+ <span>Wide</span>
155
+ </div>
156
+ </div>
157
+ </div>
158
+
159
+ <!-- Footer -->
160
+ <div class="px-6 py-4 bg-gray-50 dark:bg-gray-800 border-t border-gray-200 dark:border-gray-700">
161
+ <button
162
+ @click="resetToDefaults"
163
+ class="w-full py-2 text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white transition-colors"
164
+ >
165
+ Reset to Defaults
166
+ </button>
167
+ </div>
168
+ </div>
169
+ </div>
170
+ </Transition>
171
+ </Teleport>
172
+ </template>
173
+
174
+ <script setup lang="ts">
175
+ import { computed } from 'vue'
176
+ import { useEbookStore, type ReadingMode, type Theme } from '@/composables/useEbookStore'
177
+
178
+ const ebookStore = useEbookStore()
179
+
180
+ // Settings open state - use local computed to ensure reactivity
181
+ const isSettingsOpen = computed(() => ebookStore.settingsOpen.value)
182
+
183
+ // Computed properties for template comparisons
184
+ const currentReadingMode = computed(() => ebookStore.readingMode.value)
185
+ const currentTheme = computed(() => ebookStore.theme.value)
186
+ const currentFontSize = computed(() => ebookStore.fontSize.value)
187
+ const currentFontWeight = computed(() => ebookStore.fontWeight.value)
188
+ const currentLineHeight = computed(() => ebookStore.lineHeight.value.toFixed(1))
189
+ const currentMargin = computed(() => ebookStore.margin.value)
190
+
191
+ const readingModes = [
192
+ { value: 'scroll' as ReadingMode, label: 'Scroll', icon: '📜' },
193
+ { value: 'page' as ReadingMode, label: 'Page', icon: '📄' },
194
+ { value: 'chapter' as ReadingMode, label: 'Chapter', icon: '📑' },
195
+ { value: 'reference' as ReadingMode, label: 'Cards', icon: '💳' },
196
+ ]
197
+
198
+ const themes = [
199
+ { value: 'day' as Theme, label: 'Day', preview: 'bg-white border border-gray-300', textClass: 'text-gray-700' },
200
+ { value: 'sepia' as Theme, label: 'Sepia', preview: 'bg-amber-100', textClass: 'text-amber-800' },
201
+ { value: 'night' as Theme, label: 'Night', preview: 'bg-gray-800', textClass: 'text-gray-300' },
202
+ { value: 'oled' as Theme, label: 'OLED', preview: 'bg-black border border-gray-600', textClass: 'text-gray-300' },
203
+ ]
204
+
205
+ function resetToDefaults() {
206
+ ebookStore.setFontSize(18)
207
+ ebookStore.setFontWeight(400)
208
+ ebookStore.setLineHeight(1.6)
209
+ ebookStore.setMargin(48)
210
+ ebookStore.setTheme('day')
211
+ ebookStore.setReadingMode('scroll')
212
+ }
213
+ </script>
214
+
215
+ <style scoped>
216
+ .modal-enter-active,
217
+ .modal-leave-active {
218
+ transition: opacity 0.2s ease;
219
+ }
220
+
221
+ .modal-enter-from,
222
+ .modal-leave-to {
223
+ opacity: 0;
224
+ }
225
+
226
+ .modal-enter-active > div:last-child,
227
+ .modal-leave-active > div:last-child {
228
+ transition: transform 0.2s ease, opacity 0.2s ease;
229
+ }
230
+
231
+ .modal-enter-from > div:last-child,
232
+ .modal-leave-to > div:last-child {
233
+ transform: scale(0.95);
234
+ opacity: 0;
235
+ }
236
+ </style>
@@ -0,0 +1,135 @@
1
+ <template>
2
+ <li>
3
+ <!-- Item with children (collapsible) -->
4
+ <template v-if="item.children && item.children.length > 0">
5
+ <div class="flex items-start gap-1">
6
+ <button
7
+ @click="toggle"
8
+ class="flex-shrink-0 w-6 h-6 flex items-center justify-center p-1 text-sm text-left rounded-md hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors self-center"
9
+ :class="buttonClass"
10
+ >
11
+ <svg
12
+ class="w-3.5 h-3.5 transition-transform"
13
+ :class="{ 'rotate-90': isOpen }"
14
+ fill="none"
15
+ stroke="currentColor"
16
+ viewBox="0 0 24 24"
17
+ >
18
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
19
+ </svg>
20
+ </button>
21
+ <a
22
+ :href="'#' + item.id"
23
+ class="flex items-center gap-2 py-1 px-1 text-sm rounded-md hover:bg-gray-200 dark:hover:bg-gray-700 hover:text-gray-900 dark:hover:text-gray-100 transition-colors flex-grow truncate"
24
+ :class="linkClass"
25
+ >
26
+ <span v-if="typeLabel" class="w-8 text-center text-xs px-1 py-0.5 rounded flex-shrink-0" :class="typeBadgeClass">{{ typeLabel }}</span>
27
+ <span class="w-10 text-xs text-gray-500 dark:text-gray-400 flex-shrink-0 font-mono text-right">{{ getNumber(item.id) }}</span>
28
+ <span class="truncate">{{ item.title }}</span>
29
+ </a>
30
+ </div>
31
+ <ul v-if="isOpen" class="pl-4 mt-0.5 space-y-0.5 border-l border-gray-200 dark:border-gray-700">
32
+ <TocTreeItem
33
+ v-for="child in item.children"
34
+ :key="child.id"
35
+ :item="child"
36
+ :depth="depth + 1"
37
+ />
38
+ </ul>
39
+ </template>
40
+
41
+ <!-- Leaf item (no children) -->
42
+ <a
43
+ v-else
44
+ :href="'#' + item.id"
45
+ class="flex items-center gap-2 py-1 px-1 pl-5 text-sm rounded-md hover:bg-gray-200 dark:hover:bg-gray-700 hover:text-gray-900 dark:hover:text-gray-100 transition-colors truncate"
46
+ :class="linkClass"
47
+ >
48
+ <span v-if="typeLabel" class="w-8 text-center text-xs px-1 py-0.5 rounded flex-shrink-0" :class="typeBadgeClass">{{ typeLabel }}</span>
49
+ <span class="w-10 text-xs text-gray-500 dark:text-gray-400 flex-shrink-0 font-mono text-right">{{ getNumber(item.id) }}</span>
50
+ <span class="truncate">{{ item.title }}</span>
51
+ </a>
52
+ </li>
53
+ </template>
54
+
55
+ <script setup lang="ts">
56
+ import { ref, computed } from 'vue'
57
+ import { useDocumentStore, type TocItem } from '@/stores/documentStore'
58
+ import { useUiStore } from '@/stores/uiStore'
59
+
60
+ interface Props {
61
+ item: TocItem
62
+ depth: number
63
+ }
64
+
65
+ const props = defineProps<Props>()
66
+ const documentStore = useDocumentStore()
67
+ const uiStore = useUiStore()
68
+ const isOpen = ref(props.depth <= 1)
69
+
70
+ const typeLabel = computed(() => {
71
+ switch (props.item.type) {
72
+ case 'part': return 'Pt'
73
+ case 'chapter': return 'Ch'
74
+ case 'appendix': return 'App'
75
+ case 'section': return ''
76
+ case 'glossary': return 'Gl'
77
+ case 'bibliography': return 'Bib'
78
+ case 'index': return 'Idx'
79
+ case 'preface': return 'Pref'
80
+ case 'reference': return 'Ref'
81
+ default: return ''
82
+ }
83
+ })
84
+
85
+ const typeBadgeClass = computed(() => {
86
+ switch (props.item.type) {
87
+ case 'part':
88
+ return 'bg-purple-200 dark:bg-purple-800 text-purple-800 dark:text-purple-200 font-medium'
89
+ case 'chapter':
90
+ return 'bg-blue-200 dark:bg-blue-800 text-blue-800 dark:text-blue-200 font-medium'
91
+ case 'appendix':
92
+ return 'bg-green-200 dark:bg-green-800 text-green-800 dark:text-green-200 font-medium'
93
+ case 'section':
94
+ return 'bg-gray-100 dark:bg-gray-700 text-gray-500 dark:text-gray-400'
95
+ case 'glossary':
96
+ return 'bg-yellow-200 dark:bg-yellow-800 text-yellow-800 dark:text-yellow-200 font-medium'
97
+ case 'bibliography':
98
+ return 'bg-red-200 dark:bg-red-800 text-red-800 dark:text-red-200 font-medium'
99
+ case 'index':
100
+ return 'bg-indigo-200 dark:bg-indigo-800 text-indigo-800 dark:text-indigo-200 font-medium'
101
+ case 'preface':
102
+ return 'bg-orange-200 dark:bg-orange-800 text-orange-800 dark:text-orange-200 font-medium'
103
+ case 'reference':
104
+ return 'bg-cyan-200 dark:bg-cyan-800 text-cyan-800 dark:text-cyan-200 font-medium'
105
+ default:
106
+ return 'bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400'
107
+ }
108
+ })
109
+
110
+ const buttonClass = computed(() => {
111
+ if (props.item.type === 'part') {
112
+ return 'font-semibold text-gray-800 dark:text-gray-100'
113
+ }
114
+ return 'text-gray-700 dark:text-gray-300'
115
+ })
116
+
117
+ const linkClass = computed(() => {
118
+ const isActive = uiStore.activeSectionId === props.item.id
119
+ if (isActive) {
120
+ return 'bg-blue-100 dark:bg-blue-900 text-blue-700 dark:text-blue-300 font-medium'
121
+ }
122
+ if (props.item.type === 'part') {
123
+ return 'font-semibold text-gray-800 dark:text-gray-100'
124
+ }
125
+ return 'text-gray-600 dark:text-gray-400'
126
+ })
127
+
128
+ function toggle() {
129
+ isOpen.value = !isOpen.value
130
+ }
131
+
132
+ function getNumber(id: string): string {
133
+ return documentStore.getNumbering(id)
134
+ }
135
+ </script>
@@ -0,0 +1,191 @@
1
+ import { reactive, computed, watch } from 'vue'
2
+
3
+ export type ReadingMode = 'scroll' | 'page' | 'chapter' | 'reference'
4
+ export type Theme = 'day' | 'sepia' | 'night' | 'oled'
5
+
6
+ interface EbookState {
7
+ readingMode: ReadingMode
8
+ fontSize: number
9
+ fontWeight: number
10
+ lineHeight: number
11
+ margin: number
12
+ theme: Theme
13
+ uiVisible: boolean
14
+ tocOpen: boolean
15
+ settingsOpen: boolean
16
+ }
17
+
18
+ const STORAGE_KEY = 'docbook_ebook_preferences'
19
+
20
+ // Default state
21
+ const defaultState: EbookState = {
22
+ readingMode: 'scroll',
23
+ fontSize: 18,
24
+ fontWeight: 400,
25
+ lineHeight: 1.6,
26
+ margin: 48,
27
+ theme: 'day',
28
+ uiVisible: true,
29
+ tocOpen: false,
30
+ settingsOpen: false
31
+ }
32
+
33
+ // Load from localStorage
34
+ function loadPreferences(): Partial<EbookState> {
35
+ try {
36
+ const stored = localStorage.getItem(STORAGE_KEY)
37
+ if (stored) {
38
+ return JSON.parse(stored)
39
+ }
40
+ } catch (e) {
41
+ console.warn('Failed to load preferences:', e)
42
+ }
43
+ return {}
44
+ }
45
+
46
+ // Save to localStorage
47
+ function savePreferences(state: EbookState) {
48
+ try {
49
+ const { uiVisible, tocOpen, settingsOpen, ...persisted } = state
50
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(persisted))
51
+ } catch (e) {
52
+ console.warn('Failed to save preferences:', e)
53
+ }
54
+ }
55
+
56
+ // Global reactive state - settingsOpen ALWAYS starts false regardless of localStorage
57
+ const loadedPrefs = loadPreferences()
58
+ // Ensure settingsOpen is never true from localStorage
59
+ delete loadedPrefs.settingsOpen
60
+ const state = reactive<EbookState>(Object.assign({}, defaultState, loadedPrefs, { settingsOpen: false }))
61
+
62
+ // Auto-save when persisted fields change (not deep watch for performance)
63
+ watch(
64
+ () => ({
65
+ readingMode: state.readingMode,
66
+ fontSize: state.fontSize,
67
+ fontWeight: state.fontWeight,
68
+ lineHeight: state.lineHeight,
69
+ margin: state.margin,
70
+ theme: state.theme
71
+ }),
72
+ () => savePreferences(state),
73
+ { immediate: true }
74
+ )
75
+
76
+ // Theme application function - applies Tailwind dark class based on theme
77
+ function applyTheme() {
78
+ const isDark = state.theme === 'night' || state.theme === 'oled'
79
+ document.documentElement.classList.toggle('dark', isDark)
80
+ }
81
+
82
+ // Apply theme on load and when theme changes
83
+ applyTheme()
84
+ watch(() => state.theme, applyTheme)
85
+
86
+ export function useEbookStore() {
87
+ // Computed getters - these ARE reactive and will auto-unwrap in templates
88
+ const readingMode = computed(() => state.readingMode)
89
+ const fontSize = computed(() => state.fontSize)
90
+ const fontWeight = computed(() => state.fontWeight)
91
+ const lineHeight = computed(() => state.lineHeight)
92
+ const margin = computed(() => state.margin)
93
+ const theme = computed(() => state.theme)
94
+ const uiVisible = computed(() => state.uiVisible)
95
+ const tocOpen = computed(() => state.tocOpen)
96
+ const settingsOpen = computed(() => state.settingsOpen)
97
+
98
+ // Actions
99
+ function setReadingMode(mode: ReadingMode) {
100
+ state.readingMode = mode
101
+ }
102
+
103
+ function setFontSize(size: number) {
104
+ state.fontSize = Math.max(12, Math.min(32, size))
105
+ }
106
+
107
+ function setFontWeight(weight: number) {
108
+ state.fontWeight = Math.max(300, Math.min(700, weight))
109
+ }
110
+
111
+ function setLineHeight(height: number) {
112
+ state.lineHeight = Math.max(1.2, Math.min(2.0, height))
113
+ }
114
+
115
+ function setMargin(m: number) {
116
+ state.margin = Math.max(16, Math.min(96, m))
117
+ }
118
+
119
+ function setTheme(t: Theme) {
120
+ state.theme = t
121
+ applyTheme()
122
+ }
123
+
124
+ function setUiVisible(visible: boolean) {
125
+ state.uiVisible = visible
126
+ }
127
+
128
+ function toggleToc() {
129
+ state.tocOpen = !state.tocOpen
130
+ if (state.tocOpen) {
131
+ state.settingsOpen = false
132
+ }
133
+ }
134
+
135
+ function toggleSettings() {
136
+ state.settingsOpen = !state.settingsOpen
137
+ if (state.settingsOpen) {
138
+ state.tocOpen = false
139
+ }
140
+ }
141
+
142
+ function closeAll() {
143
+ state.tocOpen = false
144
+ state.settingsOpen = false
145
+ }
146
+
147
+ // Theme CSS class
148
+ function getThemeClass(): string {
149
+ return `theme-${state.theme}`
150
+ }
151
+
152
+ // Generate CSS variables object
153
+ function getCssVariables(): Record<string, string | number> {
154
+ return {
155
+ '--ebook-font-size': `${state.fontSize}px`,
156
+ '--ebook-font-weight': state.fontWeight,
157
+ '--ebook-line-height': state.lineHeight,
158
+ '--ebook-margin': `${state.margin}px`,
159
+ }
160
+ }
161
+
162
+ return {
163
+ // Computed getters (reactive, auto-unwrap in templates)
164
+ readingMode,
165
+ fontSize,
166
+ fontWeight,
167
+ lineHeight,
168
+ margin,
169
+ theme,
170
+ uiVisible,
171
+ tocOpen,
172
+ settingsOpen,
173
+
174
+ // Actions
175
+ setReadingMode,
176
+ setFontSize,
177
+ setFontWeight,
178
+ setLineHeight,
179
+ setMargin,
180
+ setTheme,
181
+ applyTheme,
182
+ setUiVisible,
183
+ toggleToc,
184
+ toggleSettings,
185
+ closeAll,
186
+
187
+ // Utilities
188
+ getThemeClass,
189
+ getCssVariables
190
+ }
191
+ }