Implement Bible desk study layout
This commit is contained in:
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
|
- Completed Phase 9 Bible desk layout by replacing the 50/50 reader/resource split with a resizable Bible page, verse-focused margin, and tabbed study stack.
|
||||||
|
- Added verse-anchored margin notes with markers, translation/context anchors, visible verse markers, and export metadata in JSON, Markdown, HTML, and CSV.
|
||||||
- Changed the HotView HMR host from loopback-only `127.0.0.1` to the detected Tailscale IPv4 address, with a `VITE_HMR_HOST` override for other private intranet addresses.
|
- Changed the HotView HMR host from loopback-only `127.0.0.1` to the detected Tailscale IPv4 address, with a `VITE_HMR_HOST` override for other private intranet addresses.
|
||||||
- Updated conventions and project-state notes before the planned consolidation phase.
|
- Updated conventions and project-state notes before the planned consolidation phase.
|
||||||
- Completed Phase 8 consolidation by reorganizing the roadmap around durable product tracks and documenting app architecture boundaries.
|
- Completed Phase 8 consolidation by reorganizing the roadmap around durable product tracks and documenting app architecture boundaries.
|
||||||
|
|||||||
@@ -80,6 +80,10 @@ Use these tracks for future planning. Pick one active implementation phase at a
|
|||||||
|
|
||||||
Goal: make the reader, study margin, notes, search, and resource layout feel like a serious Bible study workspace.
|
Goal: make the reader, study margin, notes, search, and resource layout feel like a serious Bible study workspace.
|
||||||
|
|
||||||
|
Current milestone:
|
||||||
|
|
||||||
|
- Phase 9: Bible desk layout and verse-anchored notes. Complete.
|
||||||
|
|
||||||
Near candidates:
|
Near candidates:
|
||||||
|
|
||||||
- Improve search result navigation and saved searches.
|
- Improve search result navigation and saved searches.
|
||||||
|
|||||||
@@ -9,10 +9,11 @@ Purpose: make the app feel like a serious Bible study workspace.
|
|||||||
Current foundation:
|
Current foundation:
|
||||||
|
|
||||||
- Page-first reader.
|
- Page-first reader.
|
||||||
- Study margin.
|
- Bible desk layout with a resizable Bible page, verse-focused margin, and tabbed study stack.
|
||||||
- Session-aware resource toggles.
|
- Session-aware resource toggles.
|
||||||
- Strong's links and definitions.
|
- Strong's links and definitions.
|
||||||
- Notes drawer.
|
- Verse-anchored margin notes with markers and export metadata.
|
||||||
|
- Notebook book in the study stack.
|
||||||
- Timeline.
|
- Timeline.
|
||||||
- Search.
|
- Search.
|
||||||
- Maps and audio controls where resources exist.
|
- Maps and audio controls where resources exist.
|
||||||
@@ -21,7 +22,7 @@ Good next milestones:
|
|||||||
|
|
||||||
- Cross-reference UX after a LibreBible package exists.
|
- Cross-reference UX after a LibreBible package exists.
|
||||||
- Better search result navigation and saved searches.
|
- Better search result navigation and saved searches.
|
||||||
- Highlights and richer note clipping.
|
- Highlights, richer note clipping, and image/link notebook blocks.
|
||||||
- Sermon/study document workflows.
|
- Sermon/study document workflows.
|
||||||
- Responsive layout and accessibility pass.
|
- Responsive layout and accessibility pass.
|
||||||
|
|
||||||
|
|||||||
@@ -78,18 +78,7 @@ Missing or immature areas include:
|
|||||||
|
|
||||||
## Recommended Next Step
|
## Recommended Next Step
|
||||||
|
|
||||||
The next phase should be consolidation, not another feature sprint.
|
After Phase 9, the next work should stay inside one product track at a time. The strongest near candidate is another Study Experience milestone focused on search result navigation, saved searches, richer clipping, and notebook document blocks. Cross-references should wait for a LibreBible cross-reference package.
|
||||||
|
|
||||||
Consolidation should turn the completed Phase 1-7 foundation into durable product tracks:
|
|
||||||
|
|
||||||
- Study experience.
|
|
||||||
- Resource ecosystem.
|
|
||||||
- Persistence and sync.
|
|
||||||
- Platform adapters.
|
|
||||||
- Public responsibility.
|
|
||||||
- Collaboration.
|
|
||||||
|
|
||||||
The goal is to preserve the platform-agnostic architecture before adding more features.
|
|
||||||
|
|
||||||
## Phase 8 Result
|
## Phase 8 Result
|
||||||
|
|
||||||
@@ -97,3 +86,14 @@ Phase 8 completed the consolidation pass. The roadmap now keeps completed founda
|
|||||||
|
|
||||||
- [Product Tracks](product-tracks.md)
|
- [Product Tracks](product-tracks.md)
|
||||||
- [App Architecture Boundaries](app-architecture-boundaries.md)
|
- [App Architecture Boundaries](app-architecture-boundaries.md)
|
||||||
|
|
||||||
|
## Phase 9 Result
|
||||||
|
|
||||||
|
Phase 9 started the Study Experience track by changing the workspace into a Bible desk:
|
||||||
|
|
||||||
|
- The default desktop layout is now Bible page, verse-focused margin, and study stack.
|
||||||
|
- The Bible and margin columns resize, and the study stack can be pinned or collapsed into a right-edge tab bar.
|
||||||
|
- Long-form resources moved into stack books: Notebook, Dictionary, Lexicon, Atlas, Commentary, and Search.
|
||||||
|
- The middle margin now focuses on the hovered or clicked verse and keeps compact verse material there.
|
||||||
|
- Margin notes can be written directly against a verse, carry markers like `!` and `?`, and preserve book/chapter/verse plus translation context for export.
|
||||||
|
- Session exports now include margin-note anchor metadata in JSON, Markdown, HTML, and CSV.
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "libre-study",
|
"name": "libre-study",
|
||||||
"version": "0.1.35",
|
"version": "0.1.36",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "libre-study",
|
"name": "libre-study",
|
||||||
"version": "0.1.35",
|
"version": "0.1.36",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tauri-apps/api": "^2.11.1",
|
"@tauri-apps/api": "^2.11.1",
|
||||||
"jszip": "^3.10.1",
|
"jszip": "^3.10.1",
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "libre-study",
|
"name": "libre-study",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.1.35",
|
"version": "0.1.36",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite --host 0.0.0.0",
|
"dev": "vite --host 0.0.0.0",
|
||||||
|
|||||||
Generated
+1
-1
@@ -77,7 +77,7 @@ checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "app"
|
name = "app"
|
||||||
version = "0.1.35"
|
version = "0.1.36"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"log",
|
"log",
|
||||||
"rusqlite",
|
"rusqlite",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "app"
|
name = "app"
|
||||||
version = "0.1.35"
|
version = "0.1.36"
|
||||||
description = "A Tauri App"
|
description = "A Tauri App"
|
||||||
authors = ["you"]
|
authors = ["you"]
|
||||||
license = ""
|
license = ""
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
|
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
|
||||||
"productName": "Libre Study",
|
"productName": "Libre Study",
|
||||||
"version": "0.1.35",
|
"version": "0.1.36",
|
||||||
"identifier": "org.librestudy.desktop",
|
"identifier": "org.librestudy.desktop",
|
||||||
"build": {
|
"build": {
|
||||||
"frontendDist": "../dist",
|
"frontendDist": "../dist",
|
||||||
|
|||||||
+308
-4
@@ -378,7 +378,7 @@ textarea:focus-visible {
|
|||||||
.study-desk.study,
|
.study-desk.study,
|
||||||
.study-desk.notes,
|
.study-desk.notes,
|
||||||
.study-desk.compare {
|
.study-desk.compare {
|
||||||
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
grid-template-columns: minmax(0, 40fr) 6px minmax(0, 20fr) 6px minmax(0, 40fr);
|
||||||
}
|
}
|
||||||
|
|
||||||
.split-resizer {
|
.split-resizer {
|
||||||
@@ -519,9 +519,10 @@ textarea:focus-visible {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.page-verse {
|
.page-verse {
|
||||||
|
position: relative;
|
||||||
break-inside: avoid;
|
break-inside: avoid;
|
||||||
margin: 0 0 9px;
|
margin: 0 0 9px;
|
||||||
padding: 3px 4px;
|
padding: 3px 4px 3px 16px;
|
||||||
color: #1f211e;
|
color: #1f211e;
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
line-height: 1.58;
|
line-height: 1.58;
|
||||||
@@ -533,6 +534,41 @@ textarea:focus-visible {
|
|||||||
background: #f4ecd8;
|
background: #f4ecd8;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.page-verse.previewed {
|
||||||
|
border-radius: 5px;
|
||||||
|
background: #f8f2e4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-verse.has-margin-note::before,
|
||||||
|
.page-verse.range-anchored::before {
|
||||||
|
position: absolute;
|
||||||
|
top: 5px;
|
||||||
|
bottom: 5px;
|
||||||
|
left: 2px;
|
||||||
|
width: 7px;
|
||||||
|
border-top: 1px solid rgba(47, 111, 94, 0.45);
|
||||||
|
border-bottom: 1px solid rgba(47, 111, 94, 0.45);
|
||||||
|
border-left: 2px solid rgba(47, 111, 94, 0.48);
|
||||||
|
border-radius: 7px 0 0 7px;
|
||||||
|
content: '';
|
||||||
|
}
|
||||||
|
|
||||||
|
.verse-note-markers {
|
||||||
|
display: inline-grid;
|
||||||
|
place-items: center;
|
||||||
|
min-width: 16px;
|
||||||
|
min-height: 16px;
|
||||||
|
margin-right: 5px;
|
||||||
|
border: 1px solid rgba(47, 111, 94, 0.32);
|
||||||
|
border-radius: 50%;
|
||||||
|
background: rgba(47, 111, 94, 0.1);
|
||||||
|
color: #225446;
|
||||||
|
font-family: ui-sans-serif, system-ui, sans-serif;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 900;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
.page-verse sup {
|
.page-verse sup {
|
||||||
margin-right: 5px;
|
margin-right: 5px;
|
||||||
color: #8a6d2e;
|
color: #8a6d2e;
|
||||||
@@ -622,7 +658,7 @@ textarea:focus-visible {
|
|||||||
|
|
||||||
.margin-resource-grid {
|
.margin-resource-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
grid-template-columns: minmax(0, 1fr);
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
align-items: start;
|
align-items: start;
|
||||||
}
|
}
|
||||||
@@ -826,10 +862,63 @@ textarea:focus-visible {
|
|||||||
.commentary-card,
|
.commentary-card,
|
||||||
.dictionary-card,
|
.dictionary-card,
|
||||||
.map-card,
|
.map-card,
|
||||||
.audio-card {
|
.audio-card,
|
||||||
|
.margin-note-card,
|
||||||
|
.compact-commentary-card,
|
||||||
|
.margin-stack-links {
|
||||||
grid-column: 1 / -1;
|
grid-column: 1 / -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.margin-note-card {
|
||||||
|
background: #fffdf8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.marker-row,
|
||||||
|
.margin-stack-links div {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.marker-row button {
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
padding: 0;
|
||||||
|
border-color: #d8cfbf;
|
||||||
|
background: #fbf8ef;
|
||||||
|
color: #225446;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
|
||||||
|
.marker-row button.active {
|
||||||
|
border-color: #2f6f5e;
|
||||||
|
background: #2f6f5e;
|
||||||
|
color: #fffef8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.margin-note-input {
|
||||||
|
min-height: 94px;
|
||||||
|
resize: vertical;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.compact-commentary-card p {
|
||||||
|
margin: 0;
|
||||||
|
color: #3f443d;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.42;
|
||||||
|
}
|
||||||
|
|
||||||
|
.compact-commentary-card button,
|
||||||
|
.margin-stack-links button {
|
||||||
|
justify-self: start;
|
||||||
|
border-color: #d8cfbf;
|
||||||
|
background: #fbf8ef;
|
||||||
|
}
|
||||||
|
|
||||||
.commentary-list,
|
.commentary-list,
|
||||||
.dictionary-list {
|
.dictionary-list {
|
||||||
display: grid;
|
display: grid;
|
||||||
@@ -1083,6 +1172,221 @@ textarea:focus-visible {
|
|||||||
gap: 14px;
|
gap: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.study-stack {
|
||||||
|
display: grid;
|
||||||
|
min-width: 0;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.study-stack.pinned {
|
||||||
|
grid-template-columns: 42px minmax(0, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.study-stack.unpinned {
|
||||||
|
position: relative;
|
||||||
|
z-index: 18;
|
||||||
|
width: 44px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.study-stack.unpinned .study-stack-panel {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
right: 44px;
|
||||||
|
width: min(560px, calc(100vw - 104px));
|
||||||
|
height: 100vh;
|
||||||
|
box-shadow: -18px 0 42px rgba(17, 20, 17, 0.24);
|
||||||
|
}
|
||||||
|
|
||||||
|
.study-stack-tabs {
|
||||||
|
display: grid;
|
||||||
|
grid-auto-rows: minmax(42px, auto);
|
||||||
|
gap: 0;
|
||||||
|
align-content: start;
|
||||||
|
min-height: 0;
|
||||||
|
border: 1px solid #d1cab9;
|
||||||
|
border-radius: 8px 0 0 8px;
|
||||||
|
background: #1f231f;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.study-stack-tabs button {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 42px;
|
||||||
|
padding: 5px 3px;
|
||||||
|
border: 0;
|
||||||
|
border-bottom: 1px solid #3c4239;
|
||||||
|
border-radius: 0;
|
||||||
|
background: #252b25;
|
||||||
|
color: #f6f3eb;
|
||||||
|
font-size: 10px;
|
||||||
|
line-height: 1.05;
|
||||||
|
writing-mode: vertical-rl;
|
||||||
|
text-orientation: mixed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.study-stack-tabs button.active {
|
||||||
|
background: #d6a84f;
|
||||||
|
color: #141510;
|
||||||
|
}
|
||||||
|
|
||||||
|
.study-stack-tabs .pin-stack {
|
||||||
|
background: #2f6f5e;
|
||||||
|
color: #fffef8;
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
|
||||||
|
.study-stack-panel {
|
||||||
|
min-width: 0;
|
||||||
|
min-height: 0;
|
||||||
|
overflow: auto;
|
||||||
|
border: 1px solid #d4cdbd;
|
||||||
|
border-left: 0;
|
||||||
|
border-radius: 0 8px 8px 0;
|
||||||
|
background: #fffef8;
|
||||||
|
box-shadow: 0 12px 30px rgba(44, 48, 42, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stack-book {
|
||||||
|
display: grid;
|
||||||
|
gap: 14px;
|
||||||
|
align-content: start;
|
||||||
|
min-height: 100%;
|
||||||
|
padding: 18px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stack-book > header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16px;
|
||||||
|
align-items: start;
|
||||||
|
padding-bottom: 12px;
|
||||||
|
border-bottom: 1px solid #e4e0d6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stack-book h2,
|
||||||
|
.stack-book h3,
|
||||||
|
.stack-book h4 {
|
||||||
|
margin: 0;
|
||||||
|
letter-spacing: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stack-book > header h2 {
|
||||||
|
color: #202124;
|
||||||
|
font-size: 24px;
|
||||||
|
line-height: 1.1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stack-book > header span:last-child {
|
||||||
|
color: #657061;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 800;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notebook-book textarea {
|
||||||
|
min-height: 360px;
|
||||||
|
resize: vertical;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-book article {
|
||||||
|
display: grid;
|
||||||
|
gap: 10px;
|
||||||
|
padding-bottom: 18px;
|
||||||
|
border-bottom: 1px solid #eee7d8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-book h3 {
|
||||||
|
color: #202124;
|
||||||
|
font-size: 24px;
|
||||||
|
line-height: 1.15;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-book h4 {
|
||||||
|
color: #657061;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 900;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-book p {
|
||||||
|
margin: 0;
|
||||||
|
color: #3f443d;
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: 1.58;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-kicker,
|
||||||
|
.article-pronunciation {
|
||||||
|
color: #657061;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wiki-article-list {
|
||||||
|
display: grid;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wiki-article-list article section {
|
||||||
|
display: grid;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stack-token-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stack-token-grid button {
|
||||||
|
display: grid;
|
||||||
|
gap: 3px;
|
||||||
|
justify-items: start;
|
||||||
|
border-color: #ded6c7;
|
||||||
|
background: #fbf8ef;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stack-token-grid button.active {
|
||||||
|
border-color: #2f6f5e;
|
||||||
|
background: #eef6f1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stack-token-grid span {
|
||||||
|
color: #657061;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-result-list {
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-result-list button {
|
||||||
|
display: grid;
|
||||||
|
gap: 4px;
|
||||||
|
justify-items: start;
|
||||||
|
padding: 10px;
|
||||||
|
border-color: #ded6c7;
|
||||||
|
background: #fbf8ef;
|
||||||
|
color: #202124;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-result-list button.active {
|
||||||
|
border-color: #2f6f5e;
|
||||||
|
background: #eef6f1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-result-list span {
|
||||||
|
color: #3f443d;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
.notes-drawer {
|
.notes-drawer {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
|
|||||||
+639
-270
File diff suppressed because it is too large
Load Diff
@@ -115,7 +115,7 @@ function encodePathSegment(path: string) {
|
|||||||
|
|
||||||
function renderMarkdown(exportData: SessionExport) {
|
function renderMarkdown(exportData: SessionExport) {
|
||||||
const session = exportData.session
|
const session = exportData.session
|
||||||
const notes = exportData.notes.filter((note) => note.body.trim())
|
const notes = exportData.notes.filter(hasExportableNoteContent)
|
||||||
return [
|
return [
|
||||||
`# ${session.title}`,
|
`# ${session.title}`,
|
||||||
'',
|
'',
|
||||||
@@ -126,7 +126,7 @@ function renderMarkdown(exportData: SessionExport) {
|
|||||||
'## Notes',
|
'## Notes',
|
||||||
'',
|
'',
|
||||||
notes.length
|
notes.length
|
||||||
? notes.map((note) => note.body.trim()).join('\n\n---\n\n')
|
? notes.map((note) => [`### ${noteHeading(note)}`, '', note.body.trim() || markerSummary(note)].join('\n')).join('\n\n---\n\n')
|
||||||
: '_No notes recorded._',
|
: '_No notes recorded._',
|
||||||
'',
|
'',
|
||||||
'## Timeline',
|
'## Timeline',
|
||||||
@@ -140,7 +140,7 @@ function renderMarkdown(exportData: SessionExport) {
|
|||||||
|
|
||||||
function renderHtml(exportData: SessionExport) {
|
function renderHtml(exportData: SessionExport) {
|
||||||
const session = exportData.session
|
const session = exportData.session
|
||||||
const notes = exportData.notes.filter((note) => note.body.trim())
|
const notes = exportData.notes.filter(hasExportableNoteContent)
|
||||||
return `<!doctype html>
|
return `<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
@@ -154,7 +154,7 @@ function renderHtml(exportData: SessionExport) {
|
|||||||
<dt>Translation</dt><dd>${escapeHtml(session.baseTranslation)}</dd>
|
<dt>Translation</dt><dd>${escapeHtml(session.baseTranslation)}</dd>
|
||||||
</dl>
|
</dl>
|
||||||
<h2>Notes</h2>
|
<h2>Notes</h2>
|
||||||
${notes.length ? notes.map((note) => `<article><pre>${escapeHtml(note.body.trim())}</pre></article>`).join('\n') : '<p>No notes recorded.</p>'}
|
${notes.length ? notes.map((note) => `<article><h3>${escapeHtml(noteHeading(note))}</h3><pre>${escapeHtml(note.body.trim() || markerSummary(note))}</pre></article>`).join('\n') : '<p>No notes recorded.</p>'}
|
||||||
<h2>Timeline</h2>
|
<h2>Timeline</h2>
|
||||||
${session.timeline.length ? `<ol>${session.timeline.map((entry) => `<li><time>${escapeHtml(entry.at)}</time> ${escapeHtml(entry.label)}${entry.reference ? ` (${escapeHtml(entry.reference)})` : ''}${entry.detail ? `: ${escapeHtml(entry.detail)}` : ''}</li>`).join('')}</ol>` : '<p>No timeline events recorded.</p>'}
|
${session.timeline.length ? `<ol>${session.timeline.map((entry) => `<li><time>${escapeHtml(entry.at)}</time> ${escapeHtml(entry.label)}${entry.reference ? ` (${escapeHtml(entry.reference)})` : ''}${entry.detail ? `: ${escapeHtml(entry.detail)}` : ''}</li>`).join('')}</ol>` : '<p>No timeline events recorded.</p>'}
|
||||||
</body>
|
</body>
|
||||||
@@ -163,7 +163,7 @@ function renderHtml(exportData: SessionExport) {
|
|||||||
|
|
||||||
function renderCsv(exportData: SessionExport) {
|
function renderCsv(exportData: SessionExport) {
|
||||||
const rows = [
|
const rows = [
|
||||||
['session_title', 'note_id', 'kind', 'created_at', 'updated_at', 'reference', 'translation_id', 'body'],
|
['session_title', 'note_id', 'kind', 'created_at', 'updated_at', 'reference', 'translation_id', 'book', 'chapter', 'start_verse', 'end_verse', 'markers', 'body'],
|
||||||
...exportData.notes.map((note) => [
|
...exportData.notes.map((note) => [
|
||||||
exportData.session.title,
|
exportData.session.title,
|
||||||
note.id,
|
note.id,
|
||||||
@@ -172,12 +172,33 @@ function renderCsv(exportData: SessionExport) {
|
|||||||
note.updatedAt,
|
note.updatedAt,
|
||||||
note.reference ?? '',
|
note.reference ?? '',
|
||||||
note.translationId ?? '',
|
note.translationId ?? '',
|
||||||
|
note.anchor?.book ?? '',
|
||||||
|
note.anchor?.chapter != null ? String(note.anchor.chapter) : '',
|
||||||
|
note.anchor?.startVerse != null ? String(note.anchor.startVerse) : '',
|
||||||
|
note.anchor?.endVerse != null ? String(note.anchor.endVerse) : '',
|
||||||
|
note.markers?.join(' ') ?? '',
|
||||||
note.body,
|
note.body,
|
||||||
]),
|
]),
|
||||||
]
|
]
|
||||||
return rows.map((row) => row.map(csvCell).join(',')).join('\n')
|
return rows.map((row) => row.map(csvCell).join(',')).join('\n')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function noteHeading(note: SessionNote) {
|
||||||
|
const kind = note.kind === 'margin' ? 'Margin note' : 'Notebook'
|
||||||
|
const reference = note.anchor?.reference ?? note.reference
|
||||||
|
const translation = note.translationId ? ` - ${note.translationId}` : ''
|
||||||
|
const markers = note.markers?.length ? ` [${note.markers.join(' ')}]` : ''
|
||||||
|
return `${kind}${reference ? ` - ${reference}` : ''}${translation}${markers}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function markerSummary(note: SessionNote) {
|
||||||
|
return note.markers?.length ? `Markers: ${note.markers.join(' ')}` : ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasExportableNoteContent(note: SessionNote) {
|
||||||
|
return Boolean(note.body.trim() || note.markers?.length)
|
||||||
|
}
|
||||||
|
|
||||||
function csvCell(value: string) {
|
function csvCell(value: string) {
|
||||||
return `"${value.replace(/"/g, '""')}"`
|
return `"${value.replace(/"/g, '""')}"`
|
||||||
}
|
}
|
||||||
|
|||||||
+25
-2
@@ -23,14 +23,25 @@ export type TimelineEntry = {
|
|||||||
detail?: string
|
detail?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type VerseAnchor = {
|
||||||
|
book: string
|
||||||
|
chapter: number
|
||||||
|
startVerse: number
|
||||||
|
endVerse: number
|
||||||
|
reference: string
|
||||||
|
translationId: string
|
||||||
|
}
|
||||||
|
|
||||||
export type SessionNote = {
|
export type SessionNote = {
|
||||||
id: string
|
id: string
|
||||||
kind: 'draft'
|
kind: 'draft' | 'margin'
|
||||||
body: string
|
body: string
|
||||||
createdAt: string
|
createdAt: string
|
||||||
updatedAt: string
|
updatedAt: string
|
||||||
reference?: string
|
reference?: string
|
||||||
translationId?: string
|
translationId?: string
|
||||||
|
anchor?: VerseAnchor
|
||||||
|
markers?: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export type StudySession = {
|
export type StudySession = {
|
||||||
@@ -73,9 +84,15 @@ export type StudySettings = {
|
|||||||
showDictionaryPanel: boolean
|
showDictionaryPanel: boolean
|
||||||
splitPercent: number
|
splitPercent: number
|
||||||
notesPinned: boolean
|
notesPinned: boolean
|
||||||
|
bibleColumnPercent: number
|
||||||
|
marginColumnPercent: number
|
||||||
|
studyStackPinned: boolean
|
||||||
|
activeStudyBook: StudyBook
|
||||||
themeMode: ThemeMode
|
themeMode: ThemeMode
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type StudyBook = 'notebook' | 'dictionary' | 'lexicon' | 'atlas' | 'commentary' | 'search'
|
||||||
|
|
||||||
export type WorkspaceSnapshot = {
|
export type WorkspaceSnapshot = {
|
||||||
schemaVersion: number
|
schemaVersion: number
|
||||||
sessions: StudySession[]
|
sessions: StudySession[]
|
||||||
@@ -130,6 +147,10 @@ export function defaultStudySettings(): StudySettings {
|
|||||||
showDictionaryPanel: true,
|
showDictionaryPanel: true,
|
||||||
splitPercent: 50,
|
splitPercent: 50,
|
||||||
notesPinned: false,
|
notesPinned: false,
|
||||||
|
bibleColumnPercent: 40,
|
||||||
|
marginColumnPercent: 20,
|
||||||
|
studyStackPinned: true,
|
||||||
|
activeStudyBook: 'notebook',
|
||||||
themeMode: 'light',
|
themeMode: 'light',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -204,12 +225,14 @@ function normalizeSessionNotes(session: Partial<StudySession>): SessionNote[] {
|
|||||||
if (Array.isArray(session.notes) && session.notes.length) {
|
if (Array.isArray(session.notes) && session.notes.length) {
|
||||||
return session.notes.map((note) => ({
|
return session.notes.map((note) => ({
|
||||||
id: note.id || timelineId(),
|
id: note.id || timelineId(),
|
||||||
kind: 'draft',
|
kind: note.kind === 'margin' ? 'margin' : 'draft',
|
||||||
body: note.body ?? '',
|
body: note.body ?? '',
|
||||||
createdAt: note.createdAt ?? session.createdAt ?? new Date().toISOString(),
|
createdAt: note.createdAt ?? session.createdAt ?? new Date().toISOString(),
|
||||||
updatedAt: note.updatedAt ?? session.updatedAt ?? new Date().toISOString(),
|
updatedAt: note.updatedAt ?? session.updatedAt ?? new Date().toISOString(),
|
||||||
reference: note.reference,
|
reference: note.reference,
|
||||||
translationId: note.translationId ?? session.baseTranslation,
|
translationId: note.translationId ?? session.baseTranslation,
|
||||||
|
anchor: note.anchor,
|
||||||
|
markers: Array.isArray(note.markers) ? note.markers : [],
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user