update borders
This commit is contained in:
1 parent
531a32e394
commit
c66f9a1e2a
12 files changed
+378
-9
No files matched your search
+190
-5
@@ -22,7 +22,11 @@
|
|||||||
*::before,
|
*::before,
|
||||||
*::after { box-sizing: border-box; }
|
*::after { box-sizing: border-box; }
|
||||||
|
|
||||||
|
/* flow-root keeps main's top margin inside the body. Let through, it moves
|
||||||
|
the body's top edge down the page, and htmx scrolls a boosted page to that
|
||||||
|
edge, not to the very top. */
|
||||||
body {
|
body {
|
||||||
|
display: flow-root;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-family: var(--font-body);
|
font-family: var(--font-body);
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
@@ -74,19 +78,200 @@ sup {
|
|||||||
a { color: var(--link); }
|
a { color: var(--link); }
|
||||||
a:hover { color: var(--link-hover); }
|
a:hover { color: var(--link-hover); }
|
||||||
|
|
||||||
|
/* The frame's measures, shared by the sidebar and the button that opens it
|
||||||
|
below a laptop. A corner is six rules square, set turned half round from how
|
||||||
|
the piece is drawn, so its small square sits outermost. The frame runs two
|
||||||
|
rules along each side, one on the edge and one 2.5 rules in, both meeting
|
||||||
|
the corner's far side. */
|
||||||
|
.sidebar-shell {
|
||||||
|
--rule: 3px;
|
||||||
|
--corner: calc(var(--rule) * 6);
|
||||||
|
--inner: calc(var(--rule) * 2.5);
|
||||||
|
--run: calc(100% - var(--corner) * 2);
|
||||||
|
--margin: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Over the text along the left edge, so the reading column stays centred on
|
||||||
|
the screen, with the same margin on every side. The dialog's own box has
|
||||||
|
no padding: a click that lands on it rather than on what is inside is a
|
||||||
|
click on the backdrop. */
|
||||||
|
.sidebar {
|
||||||
|
position: fixed;
|
||||||
|
inset: var(--margin) auto var(--margin) var(--margin);
|
||||||
|
z-index: 1;
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
max-height: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
color: var(--text);
|
||||||
|
border: 0;
|
||||||
|
background:
|
||||||
|
url("../ornaments/uniform-rule-corner-br.svg") left top / var(--corner) no-repeat,
|
||||||
|
url("../ornaments/uniform-rule-corner-bl.svg") right top / var(--corner) no-repeat,
|
||||||
|
url("../ornaments/uniform-rule-corner-tr.svg") left bottom / var(--corner) no-repeat,
|
||||||
|
url("../ornaments/uniform-rule-corner-tl.svg") right bottom / var(--corner) no-repeat,
|
||||||
|
linear-gradient(#000 0 0) var(--corner) var(--inner) / var(--run) var(--rule) no-repeat,
|
||||||
|
linear-gradient(#000 0 0) var(--corner) 0 / var(--run) var(--rule) no-repeat,
|
||||||
|
linear-gradient(#000 0 0) var(--corner) calc(100% - var(--inner)) / var(--run) var(--rule) no-repeat,
|
||||||
|
linear-gradient(#000 0 0) var(--corner) 100% / var(--run) var(--rule) no-repeat,
|
||||||
|
linear-gradient(#000 0 0) var(--inner) var(--corner) / var(--rule) var(--run) no-repeat,
|
||||||
|
linear-gradient(#000 0 0) 0 var(--corner) / var(--rule) var(--run) no-repeat,
|
||||||
|
linear-gradient(#000 0 0) calc(100% - var(--inner)) var(--corner) / var(--rule) var(--run) no-repeat,
|
||||||
|
linear-gradient(#000 0 0) 100% var(--corner) / var(--rule) var(--run) no-repeat,
|
||||||
|
var(--background);
|
||||||
|
transition: opacity .15s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Where the reading column has a margin beside it, the sidebar fills it, the
|
||||||
|
gap to the text the same as the margin round the rest. */
|
||||||
|
@media (min-width: 1200px) {
|
||||||
|
.sidebar {
|
||||||
|
width: calc((100% - var(--measure)) / 2 + var(--gutter) - var(--margin) * 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-body { padding: calc(var(--corner) + 1rem); }
|
||||||
|
|
||||||
|
.sidebar::backdrop { background: rgb(0 0 0 / .3); }
|
||||||
|
|
||||||
|
/* Away from the top it fades out unless it is under the pointer. Focus does
|
||||||
|
not hold it: a select keeps focus after it is used, which would leave the
|
||||||
|
sidebar over the text. */
|
||||||
|
.scrolled .sidebar:not(:modal, :hover) { opacity: 0; }
|
||||||
|
|
||||||
|
/* Below a laptop the sidebar waits behind a button in the corner, a box in the
|
||||||
|
frame's rule. Opened, the frame grows out of it and a copy of it, which
|
||||||
|
closes the dialog, stands in for the frame's bottom right corner. */
|
||||||
|
.sidebar-fab {
|
||||||
|
display: none;
|
||||||
|
width: 4rem;
|
||||||
|
height: 4rem;
|
||||||
|
padding: 0;
|
||||||
|
color: #000;
|
||||||
|
border: var(--rule) solid #000;
|
||||||
|
background: var(--background);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-fab::before {
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
width: 90%;
|
||||||
|
height: 90%;
|
||||||
|
margin: auto;
|
||||||
|
background: currentColor;
|
||||||
|
mask: url("../ornaments/mural-311a.svg") center / contain no-repeat;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-fab:focus-visible {
|
||||||
|
outline: 2px solid var(--focus);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-open {
|
||||||
|
position: fixed;
|
||||||
|
right: max(var(--margin), env(safe-area-inset-right));
|
||||||
|
bottom: max(var(--margin), env(safe-area-inset-bottom));
|
||||||
|
z-index: 1;
|
||||||
|
transition: opacity .15s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* It fades out away from the top of the page but can still be tapped. Only a
|
||||||
|
real pointer brings it back on hover: a touch screen keeps :hover on
|
||||||
|
whatever was last tapped, which would leave it showing. */
|
||||||
|
.scrolled .sidebar-open:not(:focus-visible) { opacity: 0; }
|
||||||
|
|
||||||
|
@media (hover: hover) {
|
||||||
|
.scrolled .sidebar-open:hover { opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-close {
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 767.98px) {
|
||||||
|
.sidebar-shell { --margin: 1rem; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Without a margin beside the reading column to hold the sidebar, it waits
|
||||||
|
behind the button. Opened, it fills the screen less the margin all round,
|
||||||
|
clear of a notch or home indicator. */
|
||||||
|
@media (max-width: 1199.98px) {
|
||||||
|
.sidebar:not(:modal) { display: none; }
|
||||||
|
.sidebar-open,
|
||||||
|
.sidebar:modal .sidebar-close { display: block; }
|
||||||
|
|
||||||
|
.sidebar:modal {
|
||||||
|
inset: max(var(--margin), env(safe-area-inset-top))
|
||||||
|
max(var(--margin), env(safe-area-inset-right))
|
||||||
|
max(var(--margin), env(safe-area-inset-bottom))
|
||||||
|
max(var(--margin), env(safe-area-inset-left));
|
||||||
|
width: auto;
|
||||||
|
max-width: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.reference {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: .25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reference-book {
|
||||||
|
font-weight: 700;
|
||||||
|
margin-right: .25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* A plain box in a hairline, square like the frame, with a small black
|
||||||
|
triangle in place of the system's arrow. */
|
||||||
|
.reference select {
|
||||||
|
appearance: none;
|
||||||
|
padding: .25rem 1.75rem .25rem .5rem;
|
||||||
|
font: inherit;
|
||||||
|
color: #000;
|
||||||
|
border: 1px solid #000;
|
||||||
|
border-radius: 0;
|
||||||
|
background:
|
||||||
|
url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 10 6'%3E%3Cpath d='M0 0h10L5 6z'/%3E%3C/svg%3E")
|
||||||
|
right .5rem center / .625rem no-repeat,
|
||||||
|
var(--background);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reference select:focus-visible {
|
||||||
|
outline: 2px solid var(--focus);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.visually-hidden {
|
||||||
|
position: absolute;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
overflow: hidden;
|
||||||
|
clip-path: inset(50%);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
/* One reading column: the full width less a gutter on anything narrower than
|
/* One reading column: the full width less a gutter on anything narrower than
|
||||||
a laptop, and a measure that grows a step with the screen beyond that. */
|
a laptop, and a measure that grows a step with the screen beyond that. The
|
||||||
|
sidebar sizes itself from the same two values. */
|
||||||
|
:root { --gutter: .75rem; }
|
||||||
|
|
||||||
main {
|
main {
|
||||||
margin: 1.5rem auto;
|
margin: 1.5rem auto;
|
||||||
padding: 0 .75rem;
|
padding: 0 var(--gutter);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 1200px) {
|
@media (min-width: 1200px) {
|
||||||
main { max-width: 760px; }
|
:root { --measure: 760px; }
|
||||||
|
main { max-width: var(--measure); }
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 1400px) {
|
@media (min-width: 1400px) {
|
||||||
main { max-width: 880px; }
|
:root { --measure: 880px; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Only A-Z and a-z: the compiler puts a drop cap on a chapter only when it
|
/* Only A-Z and a-z: the compiler puts a drop cap on a chapter only when it
|
||||||
@@ -196,7 +381,7 @@ main {
|
|||||||
gap: .4em;
|
gap: .4em;
|
||||||
font-size: 2rem;
|
font-size: 2rem;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
color: var(--text-muted);
|
color: var(--text);
|
||||||
margin: 5rem 0 .75rem;
|
margin: 5rem 0 .75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+10
-2
@@ -11,6 +11,9 @@
|
|||||||
'page.njk',
|
'page.njk',
|
||||||
'book.njk',
|
'book.njk',
|
||||||
'index.njk',
|
'index.njk',
|
||||||
|
'sidebar.njk',
|
||||||
|
'reference.njk',
|
||||||
|
'verses.njk',
|
||||||
'version.njk',
|
'version.njk',
|
||||||
'notice.njk'
|
'notice.njk'
|
||||||
];
|
];
|
||||||
@@ -57,11 +60,16 @@
|
|||||||
return {
|
return {
|
||||||
render: render,
|
render: render,
|
||||||
|
|
||||||
compose: function (fragment) {
|
compose: function (fragment, chrome) {
|
||||||
var title = (TITLE_RE.exec(fragment) || ['', ''])[1].trim();
|
var title = (TITLE_RE.exec(fragment) || ['', ''])[1].trim();
|
||||||
var content = fragment.replace(TITLE_RE, '');
|
var content = fragment.replace(TITLE_RE, '');
|
||||||
var name = (TEMPLATE_RE.exec(content) || ['', 'page'])[1] + '.njk';
|
var name = (TEMPLATE_RE.exec(content) || ['', 'page'])[1] + '.njk';
|
||||||
return render(name, { title: title, content: content });
|
return render(name, {
|
||||||
|
title: title,
|
||||||
|
content: content,
|
||||||
|
sidebar: chrome.sidebar,
|
||||||
|
reference: chrome.reference
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
notice: function (heading, text) {
|
notice: function (heading, text) {
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 450 450" width="450" height="450" fill="currentColor"><path d="M222.2 28.1C219.6 28.8 218.4 30.3 216.5 35.7C216.2 36.5 215.3 38 214.5 39C213.8 40.1 212.9 41.4 212.5 42.1C212.2 42.8 210.9 44.3 209.7 45.5C205.1 49.8 202.6 53 201.8 55.4C201.4 56.7 200.7 58.4 200.2 59.3C199.3 61.5 199.2 63.9 200.1 66.8C201.7 71.9 200.1 75.4 194.9 77.7C193.6 78.2 192.1 79.1 191.5 79.5C190.4 80.3 186.8 84.9 186 86.5C185.7 87 184.4 88.5 183 90C180.5 92.5 180.2 92.7 176.3 94.4C174.1 95.3 171.3 96.3 170.2 96.4C167.9 96.8 167.1 97.3 166.5 98.9C165.8 100.6 166.4 102 168.7 104.6C169.9 105.9 170.9 107.1 170.9 107.2C170.9 107.4 172 108.3 173.5 109.2C176.6 111.4 178.9 111.9 184.4 111.9C187.5 111.9 188.8 112.1 190.3 112.6C191.4 112.9 193.5 113.4 195.1 113.6C199.2 114.1 200.4 115.1 200 117.9C199.7 120.5 198.2 121.7 194.7 122C192.2 122.2 191.6 122.4 189.9 123.5C188.8 124.2 187 125 186 125.3C182.9 126.1 179.2 128.3 175 131.7C172.3 133.8 169.6 133.5 169.6 131.1C169.6 128.5 172.5 121.7 175 118.7C176.5 116.9 176.6 115.9 175.4 114.1C174.3 112.4 174.3 112.4 171.1 111.4C168.5 110.5 168 110.5 162.2 110.5C154.9 110.4 150.6 111.2 147.4 113.1C143.7 115.3 142.7 115 138.8 111C136.9 109 135.8 108.1 134.6 107.7C133.8 107.4 131.9 106.6 130.4 106C128.2 105.2 127.1 104.9 124.4 104.8C121.8 104.6 120.2 104.3 117.3 103.4C109.9 101 108.1 102.3 109 109.2C109.3 111.4 109.3 112.9 109 115.4C108.5 119.7 108.8 128 109.5 130.3C110.1 132 110.6 132.9 112.6 135.3C114.2 137.3 114.1 141 112.4 144.7C109.2 151.7 108.5 155.7 109.4 163.6C109.6 166 109.8 166.6 110.6 167.4C112.9 169.8 114.2 169.9 119.6 167.7C123.2 166.2 127.6 165.4 132.8 165.3C136.3 165.2 136.7 165.6 135.2 168.5C133.4 172.1 129.2 181.9 128.3 184.5C122.8 201.5 123.9 200 117.2 200C109.9 200 104.3 199.2 102.4 198.1C99.9 196.5 98.9 194.2 99.1 190.6C99.4 186.3 99.9 183.4 101 178.6C102.3 173.3 102.3 171.3 100.7 166.4C98.4 159.3 94 155.1 90.5 156.8C89.5 157.3 87.2 160 85.2 163.1C84.2 164.7 80.9 167.8 79.5 168.5C79 168.8 77.6 169.3 76.3 169.6C75 169.9 73.4 170.5 72.6 170.9C71.8 171.3 70.4 172 69.4 172.4C67.4 173.2 64.2 175.9 62 179C61.3 180 60.6 180.8 60.3 181C59.5 181.5 58.9 183.5 58.6 186.4C58.3 190.9 57.7 195.3 57.3 196.2C56.8 197.4 54.2 198.4 52 198.2C49.6 197.8 46.5 198.7 42 200.8C38.3 202.6 38.3 202.6 33.4 207.9C28.3 213.3 25.6 215.5 21.2 217.7C18.4 219.2 17.7 220.1 17.5 222.3C17 226 19.3 229 23.9 230.6C27.3 231.9 29.8 234.3 32.3 238.5C34.2 241.7 35.8 243.3 38.7 245C39.6 245.5 40.9 246.5 41.5 247.2C42.7 248.4 47.7 251.5 49.8 252.3C50.3 252.6 52.4 253.1 54.4 253.6C60.1 254.9 60.8 255.4 61.2 258.9C61.4 260.5 61.7 261.1 63 262.8C63.8 263.9 64.8 265.2 65.1 265.6C65.7 266.4 69.2 269 70.8 269.9C71.2 270.1 72.5 270.5 73.8 270.9C78.6 272.2 83.8 276.5 87.1 281.8C88.1 283.4 89.3 285.1 89.7 285.6C91.1 287.1 93.4 286.8 95 284.6C95.4 284.1 96.4 282.9 97.2 282C98.6 280.3 99.6 278.2 100.5 274.5C101.1 272.3 101.1 271 100.5 260.2C100.2 256 100.5 254.7 102.1 252.9C103.7 251.1 107.4 250.4 115.9 250.4C120.1 250.4 120.1 250.4 121.3 251.4C122.8 252.7 123.9 254.5 125.4 258.3C126 259.9 127.3 262.4 128.1 263.8C128.9 265.2 129.7 267 129.9 267.7C130.3 269.2 133.7 274.7 135.5 276.7C136.8 278.1 136.9 278.9 136 279.7C134.2 281.3 121 278.6 114.4 275.2C112.9 274.4 111.7 274.4 110.4 275.1C108.5 276 108.4 276.5 108.4 283.3C108.4 290 108.8 293.9 110.2 299.7C111.6 305.2 111.5 305.8 107.8 310.1C105.2 313.2 105 314.4 105.2 321.6C105.4 327.5 105.4 327.5 104.4 330.5C103 334.4 103.1 335.7 104.4 337.3C106.4 339.5 107.5 339.8 115.9 340.1C120 340.3 124.2 340.5 125.3 340.6C127.8 340.9 130.2 340.2 132.6 338.5C136.5 335.7 137.4 335.6 142.1 337.1C147.1 338.6 148 338.8 151.6 338.4C153.5 338.2 157.4 338 160.3 337.9C168.4 337.7 174.2 336.4 175 334.5C175.4 333.6 174.4 330.3 172.3 325.9C170.3 321.7 169.2 314.8 170.3 313.6C171.2 312.7 173.7 313.1 176 314.6C178.9 316.5 183.5 318.5 187.4 319.5C189.1 319.9 191.1 320.5 191.8 320.8C192.5 321 193.6 321.3 194.2 321.3C195.5 321.3 197.7 322.3 198.7 323.3C201.4 326.3 202.5 338.2 200.3 342.6C198.8 345.8 197.2 347.1 195.2 347.1C192.3 347.1 188.6 346.3 185 344.8C180.9 343.1 177.3 342.7 172.6 343.4C167.8 344.2 165.9 345.4 165.6 348C165.4 349.5 165.5 349.6 167.3 351.3C170.6 354.4 172.4 357.7 175.3 365.8C176 367.8 177 370.1 177.5 370.9C178.4 372.5 182.4 376.3 184.8 377.9C186.8 379.2 196.4 383.4 197.4 383.4C199.4 383.4 201 386.4 201.5 390.8C202 395.4 202.6 396.8 205.2 400.1C206.5 401.8 208.1 403.5 208.8 404.1C209.5 404.6 211.4 406.4 213.1 408C214.8 409.5 216.9 411.4 217.8 412.1C218.7 412.8 220.2 414.1 220.9 415.2C221.7 416.2 223.5 418.2 224.9 419.7C227.3 422.1 227.7 422.4 228.8 422.4C231.3 422.4 232.8 421.1 233.8 418.1C234.2 416.9 234.9 415.3 235.4 414.5C235.9 413.8 236.6 412.5 236.9 411.7C237.8 409.4 238.2 408.8 240.3 407.5C241.3 406.8 242.3 405.9 242.6 405.6C242.8 405.3 244.5 403.6 246.3 401.9C248.2 400.2 250 398.3 250.5 397.6C251.3 396.5 252.6 391.7 253.1 388.6C253.5 385.6 254 385 259 382.6C264.2 380.1 267.9 377.1 268.7 374.5C268.9 373.8 269.6 372.3 270.1 371.2C270.7 370.1 271.4 Line truncated
|
||||||
|
After Width: | Height: | Size: 22 KiB |
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 600 600" fill="currentColor" fill-rule="evenodd"><path d="M0 0H100V250H250V0H600V350H350V500H600V600H0Z M350 100V250H500V100Z M100 350V500H250V350Z"/></svg>
|
||||||
|
After Width: | Height: | Size: 210 B |
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 600 600" fill="currentColor" fill-rule="evenodd"><g transform="translate(600,0) scale(-1,1)"><path d="M0 0H100V250H250V0H600V350H350V500H600V600H0Z M350 100V250H500V100Z M100 350V500H250V350Z"/></g></svg>
|
||||||
|
After Width: | Height: | Size: 258 B |
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 600 600" fill="currentColor" fill-rule="evenodd"><g transform="translate(0,600) scale(1,-1)"><path d="M0 0H100V250H250V0H600V350H350V500H600V600H0Z M350 100V250H500V100Z M100 350V500H250V350Z"/></g></svg>
|
||||||
|
After Width: | Height: | Size: 258 B |
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 600 600" fill="currentColor" fill-rule="evenodd"><g transform="translate(600,600) scale(-1,-1)"><path d="M0 0H100V250H250V0H600V350H350V500H600V600H0Z M350 100V250H500V100Z M100 350V500H250V350Z"/></g></svg>
|
||||||
|
After Width: | Height: | Size: 261 B |
+102
-2
@@ -24,6 +24,11 @@ const PRECACHE = [
|
|||||||
'./css/app.css',
|
'./css/app.css',
|
||||||
'./fonts/DayLeafInitials.woff2',
|
'./fonts/DayLeafInitials.woff2',
|
||||||
'./ornaments/group-l-49.svg',
|
'./ornaments/group-l-49.svg',
|
||||||
|
'./ornaments/mural-311a.svg',
|
||||||
|
'./ornaments/uniform-rule-corner-tl.svg',
|
||||||
|
'./ornaments/uniform-rule-corner-tr.svg',
|
||||||
|
'./ornaments/uniform-rule-corner-bl.svg',
|
||||||
|
'./ornaments/uniform-rule-corner-br.svg',
|
||||||
'./js/htmx.min.js'
|
'./js/htmx.min.js'
|
||||||
].concat(TEMPLATE_URLS);
|
].concat(TEMPLATE_URLS);
|
||||||
|
|
||||||
@@ -123,6 +128,71 @@ const view = createRenderer(readTemplate);
|
|||||||
|
|
||||||
const router = new Router(new URL('./', location.href).pathname);
|
const router = new Router(new URL('./', location.href).pathname);
|
||||||
|
|
||||||
|
const VERSES_URL = './verses.json';
|
||||||
|
|
||||||
|
let verses = null;
|
||||||
|
|
||||||
|
function readVerses() {
|
||||||
|
if (!verses) {
|
||||||
|
verses = readText(pageRequest(VERSES_URL))
|
||||||
|
.then(JSON.parse)
|
||||||
|
.catch((err) => { verses = null; throw err; });
|
||||||
|
}
|
||||||
|
return verses;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseAnchor(id) {
|
||||||
|
const chapter = /^ch(\d+)$/.exec(id || '');
|
||||||
|
if (chapter) return { chapter: +chapter[1], verse: null };
|
||||||
|
const verse = /^v(\d+)_[0-9-]+$/.exec(id || '');
|
||||||
|
if (verse) return { chapter: +verse[1], verse: id };
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function verseLabels(chapters, number) {
|
||||||
|
const entry = chapters[number - 1];
|
||||||
|
if (typeof entry !== 'number') return entry || [];
|
||||||
|
const out = [];
|
||||||
|
for (let i = 1; i <= entry; i++) out.push(String(i));
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
function referenceFor(map, code, anchor) {
|
||||||
|
const chapters = map && Object.prototype.hasOwnProperty.call(map, code)
|
||||||
|
? map[code] : null;
|
||||||
|
if (!chapters || !chapters.length) return null;
|
||||||
|
|
||||||
|
if (!chapters.some((entry) => (typeof entry === 'number' ? entry : entry.length))) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const at = parseAnchor(anchor) || { chapter: 1, verse: null };
|
||||||
|
const number = at.chapter >= 1 && at.chapter <= chapters.length ? at.chapter : 1;
|
||||||
|
const verse = at.chapter === number ? at.verse : null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
book: code,
|
||||||
|
chapter: 'ch' + number,
|
||||||
|
chapters: chapters.map((_, i) => ({ id: 'ch' + (i + 1), label: String(i + 1) })),
|
||||||
|
verse: verse,
|
||||||
|
verses: verseLabels(chapters, number)
|
||||||
|
.map((label) => ({ id: 'v' + number + '_' + label, label: label }))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function bookOf(url) {
|
||||||
|
return url ? router.relative(url).slice(1) : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function referenceOf(code, anchor) {
|
||||||
|
if (!code) return null;
|
||||||
|
try {
|
||||||
|
return referenceFor(await readVerses(), code, anchor);
|
||||||
|
} catch (err) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const OFFLINE_NOTICE = ['Not available offline',
|
const OFFLINE_NOTICE = ['Not available offline',
|
||||||
'This page has not been opened on this device, so there is no copy stored ' +
|
'This page has not been opened on this device, so there is no copy stored ' +
|
||||||
'here. Reconnect and try again, or open one you have read before.'];
|
'here. Reconnect and try again, or open one you have read before.'];
|
||||||
@@ -143,9 +213,13 @@ async function servePage(path, code, call) {
|
|||||||
if (!code) {
|
if (!code) {
|
||||||
fragment += '\n' + await view.render('version.njk', { version: VERSIONS });
|
fragment += '\n' + await view.render('version.njk', { version: VERSIONS });
|
||||||
}
|
}
|
||||||
if (!call.navigate && !call.restore) return fragment;
|
const chrome = { sidebar: !!code, reference: await referenceOf(code, null) };
|
||||||
|
if (!call.navigate && !call.restore) {
|
||||||
|
return fragment + '\n'
|
||||||
|
+ await view.render('sidebar.njk', Object.assign({ oob: true }, chrome));
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
return await view.compose(fragment);
|
return await view.compose(fragment, chrome);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('compose failed: ' + err.message);
|
console.error('compose failed: ' + err.message);
|
||||||
return fragment;
|
return fragment;
|
||||||
@@ -156,6 +230,32 @@ router.get('/', (call) => servePage('./', '', call));
|
|||||||
router.get('/:page', (call) =>
|
router.get('/:page', (call) =>
|
||||||
servePage('./' + call.params.page, call.params.page, call));
|
servePage('./' + call.params.page, call.params.page, call));
|
||||||
|
|
||||||
|
router.get('/api/reference', async (call) => {
|
||||||
|
const params = call.url.searchParams;
|
||||||
|
const page = call.currentUrl();
|
||||||
|
const moved = params.has('at') ? params.get('at') : null;
|
||||||
|
|
||||||
|
const code = bookOf(page);
|
||||||
|
const reference = await referenceOf(
|
||||||
|
code, moved === null ? (page && page.hash.slice(1)) : moved);
|
||||||
|
|
||||||
|
if (moved === null) {
|
||||||
|
return view.render('sidebar.njk',
|
||||||
|
{ sidebar: !!code, reference: reference, settled: true });
|
||||||
|
}
|
||||||
|
if (!reference) return new Response('', { status: 204 });
|
||||||
|
|
||||||
|
const target = reference.verse || reference.chapter;
|
||||||
|
return new Response(await view.render('verses.njk', { reference: reference }), {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'text/html; charset=utf-8',
|
||||||
|
'Cache-Control': 'no-store',
|
||||||
|
'HX-Reswap': 'innerHTML settle:0 show:#' + target + ':top',
|
||||||
|
'HX-Replace-Url': page.pathname + page.search + '#' + target
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
let filling = null;
|
let filling = null;
|
||||||
let complete = false;
|
let complete = false;
|
||||||
|
|
||||||
|
|||||||
@@ -21,9 +21,23 @@
|
|||||||
<body class="{% block body_class %}{% endblock %}"
|
<body class="{% block body_class %}{% endblock %}"
|
||||||
hx-boost="true" hx-target="#content" hx-swap="innerHTML show:window:top">
|
hx-boost="true" hx-target="#content" hx-swap="innerHTML show:window:top">
|
||||||
|
|
||||||
|
{% include "sidebar.njk" %}
|
||||||
|
|
||||||
<main id="content" hx-history-elt>
|
<main id="content" hx-history-elt>
|
||||||
{% block content %}{% endblock %}
|
{% block content %}{% endblock %}
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// The sidebar, and on a phone its button, fade out once the page leaves the
|
||||||
|
// top, so they are out of the way while reading; the stylesheet brings the
|
||||||
|
// sidebar back under the pointer.
|
||||||
|
(function () {
|
||||||
|
var root = document.documentElement;
|
||||||
|
function update() { root.classList.toggle('scrolled', window.scrollY > 0); }
|
||||||
|
addEventListener('scroll', update, { passive: true });
|
||||||
|
update();
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<div class="reference" id="reference">
|
||||||
|
<span class="reference-book" id="reference-book">{{ reference.book }}</span>
|
||||||
|
<label class="visually-hidden" for="reference-chapter">Chapter</label>
|
||||||
|
<select class="reference-chapter" id="reference-chapter" name="at"
|
||||||
|
hx-get="api/reference" hx-target="#reference-verse"
|
||||||
|
hx-swap="innerHTML">
|
||||||
|
{%- for chapter in reference.chapters %}
|
||||||
|
<option value="{{ chapter.id }}"
|
||||||
|
{%- if chapter.id == reference.chapter %} selected{% endif %}>{{ chapter.label }}</option>
|
||||||
|
{%- endfor %}
|
||||||
|
</select>
|
||||||
|
<span class="reference-sep">:</span>
|
||||||
|
<label class="visually-hidden" for="reference-verse">Verse</label>
|
||||||
|
<select class="reference-verse" id="reference-verse" name="at"
|
||||||
|
hx-get="api/reference" hx-target="this" hx-swap="innerHTML">
|
||||||
|
{% include "verses.njk" %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
{# Only the /:page routes have one. The element is always there, hidden on the
|
||||||
|
others, so that a boosted link in either direction has something to swap
|
||||||
|
out of band, and so a history restore can ask for the right one. #}
|
||||||
|
<div class="sidebar-shell" id="sidebar"
|
||||||
|
{%- if not sidebar %} hidden{% endif %}
|
||||||
|
{%- if oob %} hx-swap-oob="true"{% endif %}
|
||||||
|
hx-get="api/reference"
|
||||||
|
{# htmx keeps an event only when the filter returns true exactly, so a
|
||||||
|
string is discarded. #}
|
||||||
|
hx-trigger="{% if reference and not settled %}load[!!location.hash], {% endif %}htmx:historyRestore from:body"
|
||||||
|
hx-target="this" hx-swap="outerHTML">
|
||||||
|
{# Always open: beside the text on a wide screen, and hidden by the
|
||||||
|
stylesheet on a phone until the button below opens it as a modal. Closing
|
||||||
|
the modal opens it again the plain way, so it is there after a resize.
|
||||||
|
Its box has no padding, so a click on the dialog itself is the backdrop.
|
||||||
|
autofocus has opening focus the dialog itself; otherwise it goes to the
|
||||||
|
first select, and a phone opens the select's picker with it. #}
|
||||||
|
<dialog class="sidebar" open autofocus aria-label="Contents"
|
||||||
|
hx-on:click="if (event.target === this && this.matches(':modal')) this.close();"
|
||||||
|
hx-on:close="if (!this.open) this.show();">
|
||||||
|
<div class="sidebar-body">
|
||||||
|
{%- if reference %}
|
||||||
|
{% include "reference.njk" %}
|
||||||
|
{%- endif %}
|
||||||
|
</div>
|
||||||
|
{# Takes the place of the frame's last corner on a phone, right where the
|
||||||
|
button that opened it was. #}
|
||||||
|
<button class="sidebar-fab sidebar-close" type="button" aria-label="Close"
|
||||||
|
hx-on:click="this.closest('dialog').close();"></button>
|
||||||
|
</dialog>
|
||||||
|
<button class="sidebar-fab sidebar-open" type="button" aria-label="Contents"
|
||||||
|
hx-on:click="var dialog = this.previousElementSibling;
|
||||||
|
dialog.close();
|
||||||
|
dialog.showModal();"></button>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
{% for verse in reference.verses -%}
|
||||||
|
<option value="{{ verse.id }}"
|
||||||
|
{%- if verse.id == reference.verse %} selected{% endif %}>{{ verse.label }}</option>
|
||||||
|
{% endfor -%}
|
||||||
Reference in new issue
Block a user