diff --git a/resources/lang/en.json b/resources/lang/en.json
index 223daa9..1a16e6c 100644
--- a/resources/lang/en.json
+++ b/resources/lang/en.json
@@ -47,7 +47,7 @@
"loading": "Turning pages…",
"today": "Today",
"go_to_writing": "Back to today's page",
- "empty_entry": "Blank page",
+ "new_entry": "Start a new page",
"earlier": "Earlier entries",
"later": "Later entries"
},
diff --git a/resources/lang/fr.json b/resources/lang/fr.json
index 7c605aa..3be4783 100644
--- a/resources/lang/fr.json
+++ b/resources/lang/fr.json
@@ -47,7 +47,7 @@
"loading": "On tourne les pages…",
"today": "Aujourd'hui",
"go_to_writing": "Revenir à la page du jour",
- "empty_entry": "Page blanche",
+ "new_entry": "Commencer une nouvelle page",
"earlier": "Entrées précédentes",
"later": "Entrées suivantes"
},
diff --git a/src/App.vue b/src/App.vue
index 96828eb..b9b50f3 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -35,6 +35,7 @@ export default {
bCalendarOpen: false,
bSettingsOpen: false,
bMenuOpen: false,
+ bSigningOut: false,
bRailOpen: false,
iCurrentId: 0,
sCurrentDay: '',
@@ -127,13 +128,51 @@ export default {
this.$refs.book?.goToWriting();
},
+ /**
+ * The + at the end of the rail: seal the page being written and turn to
+ * a fresh one. Landing there should mean you can simply write, the same
+ * way opening the book does.
+ */
+ async onNewEntry() {
+ this.bRailOpen = false;
+
+ try {
+ await this.journal.startNewEntry();
+ this.$nextTick(() => this.$refs.book?.goToWriting());
+ }
+ catch(oError) {
+ this.notify(oError.desc_lang_text || oError.message, true);
+ }
+ },
+
/* Account */
onSignedIn(asUser) {
Object.assign(this.user, asUser);
},
- async onSignedOut() {
+ /**
+ * The logout call is what ends the session; the local reset only
+ * follows it, so a failed call leaves the reader where they were.
+ */
+ async signOut() {
+ if(this.bSigningOut) return;
+
+ this.bSigningOut = true;
+
+ try {
+ await this.api.post('logout');
+ this.clearSession();
+ }
+ catch(oError) {
+ this.notify(oError.desc_lang_text || oError.message || this.lang.get('error.unexpected'), true);
+ }
+ finally {
+ this.bSigningOut = false;
+ }
+ },
+
+ clearSession() {
this.bSettingsOpen = false;
this.bMenuOpen = false;
@@ -259,6 +298,16 @@ export default {
{{ lang.get('account.settings') }}
+
+
@@ -287,6 +336,7 @@ export default {
:loading="journal.loading"
@pick="onPickBookmark"
@load-older="onLoadOlder"
+ @new="onNewEntry"
/>
@@ -304,7 +354,6 @@ export default {
diff --git a/src/components/BookmarkRail.vue b/src/components/BookmarkRail.vue
index 7f73285..8b18f0a 100644
--- a/src/components/BookmarkRail.vue
+++ b/src/components/BookmarkRail.vue
@@ -40,6 +40,10 @@ const RAIL_REACH = 3;
* Each is stamped with the date and time the entry was started, in the timezone
* it was written in - so a book kept across a move still reads as the local
* time of each moment. Clicking one turns the book to that page.
+ *
+ * A page nothing has been written on is not one of them: an empty tab marks
+ * nothing, and a book of them would be a rail of blanks. The + at the end of
+ * the rail stands there instead, and starts the page it would have marked.
*/
export default {
components: {
@@ -54,15 +58,18 @@ export default {
hasOlder: {type: Boolean, default: false},
loading: {type: Boolean, default: false}
},
- emits: ['pick', 'load-older'],
+ emits: ['pick', 'load-older', 'new'],
inject: ['lang'],
computed: {
tabs() {
- return this.bookmarks.map((oBookmark) => ({
+ //A bookmark is a page with something on it. The one page that can be
+ //empty is the one being written, and it gets its tab with its first
+ //words - closed entries are never blank, the book discards those.
+ return this.bookmarks.filter((oBookmark) => (oBookmark.preview || '') !== '').map((oBookmark) => ({
id: oBookmark.id,
date: formatShortDate(oBookmark.time, oBookmark.timezone, this.lang.locale),
time: formatTime(oBookmark.time, oBookmark.timezone, this.lang.locale),
- preview: oBookmark.preview || this.lang.get('book.empty_entry'),
+ preview: oBookmark.preview,
open: (oBookmark.status === 'open'),
dye: dyeForDay(getDayKey(oBookmark.time, oBookmark.timezone))
}));
@@ -89,6 +96,16 @@ export default {
},
hasAfter() {
return (this.shown.length > 0) && (this.shown.at(-1).id !== this.tabs.at(-1)?.id);
+ },
+ /**
+ * Whether the book is open at a page still to be written on.
+ *
+ * That page has no tab, so the + is what marks it - and the rail would
+ * otherwise show nothing at all as the place you are reading.
+ */
+ onBlankPage() {
+ const oOpen = this.bookmarks.find((oBookmark) => oBookmark.id === this.openId);
+ return (this.currentId === this.openId) && (oOpen !== undefined) && ((oOpen.preview || '') === '');
}
},
watch: {
@@ -162,6 +179,19 @@ export default {
{{ oTab.preview }}
+
+
+
{{ lang.get('action.close') }}
-
diff --git a/src/scripts/icons.js b/src/scripts/icons.js
index 7377f1a..ca31132 100644
--- a/src/scripts/icons.js
+++ b/src/scripts/icons.js
@@ -38,16 +38,10 @@ const asIcons = {
'M12 11.6a3.6 3.6 0 1 0 0-7.2 3.6 3.6 0 0 0 0 7.2z',
'M4.8 20c.6-3.7 3.5-5.8 7.2-5.8s6.6 2.1 7.2 5.8'
],
+ //A cog: six teeth around a hub
settings: [
- 'M12 15.2a3.2 3.2 0 1 0 0-6.4 3.2 3.2 0 0 0 0 6.4z',
- 'M12 3v2.3',
- 'M12 18.7V21',
- 'M3 12h2.3',
- 'M18.7 12H21',
- 'M5.6 5.6l1.7 1.7',
- 'M16.7 16.7l1.7 1.7',
- 'M18.4 5.6l-1.7 1.7',
- 'M7.3 16.7l-1.7 1.7'
+ 'M10.3 6L10.4 3.1h3.2L13.7 6a6.2 6.2 0 0 1 2.6 1.5l2.6-1.3 1.6 2.7-2.5 1.6a6.2 6.2 0 0 1 0 3l2.5 1.6-1.6 2.7-2.6-1.3a6.2 6.2 0 0 1-2.6 1.5l-.1 2.9h-3.2L10.3 18a6.2 6.2 0 0 1-2.6-1.5l-2.6 1.3-1.6-2.7 2.5-1.6a6.2 6.2 0 0 1 0-3L3.5 8.9l1.6-2.7 2.6 1.3A6.2 6.2 0 0 1 10.3 6z',
+ 'M12 14.8a2.8 2.8 0 1 0 0-5.6 2.8 2.8 0 0 0 0 5.6z'
],
signOut: [
'M14.4 8V5.2H5.6v13.6h8.8V16',
@@ -61,6 +55,8 @@ const asIcons = {
'M10.5 10.9v5.7',
'M13.5 10.9v5.7'
],
+ //A blank page waiting to be started
+ plus: ['M12 5.6v12.8', 'M5.6 12h12.8'],
close: ['M6.6 6.6l10.8 10.8', 'M17.4 6.6 6.6 17.4'],
check: ['M5.6 12.7l4.2 4.2 8.6-9.6'],
alert: [
diff --git a/src/scripts/journal.js b/src/scripts/journal.js
index e508bf9..755b6e6 100644
--- a/src/scripts/journal.js
+++ b/src/scripts/journal.js
@@ -172,6 +172,33 @@ export default class Journal {
return oEntry;
}
+ /**
+ * Start a page of one's own, rather than writing on in the last one.
+ *
+ * A page nothing has been written on is already that page, so it is handed
+ * straight back - it is only a page with writing on it that has to be sealed
+ * first, which is what makes the next one a new entry and not the same
+ * thought carried on.
+ */
+ async startNewEntry() {
+ const oEntry = this.openEntry;
+ if(oEntry && (oEntry.content.trim() === '')) return oEntry;
+
+ //The close carries the content itself, but the save state goes with it -
+ //flushing here is what keeps the indicator from sitting on "unsaved"
+ //about a page that has already been sealed.
+ if(this.isDirty) await this.save();
+
+ await this.closeEntry();
+
+ //A new page goes at the end of the book, so the loaded window has to be
+ //the end of the book before one can be appended to it - reading back in
+ //the journal would otherwise slot it into the middle of the flow.
+ if(this.hasNewer) await this.load();
+
+ return this.startWriting();
+ }
+
write(sContent) {
const oEntry = this.openEntry;
if(!oEntry) return;
diff --git a/src/styles/_app.scss b/src/styles/_app.scss
index 8d712ee..f1ab72f 100644
--- a/src/styles/_app.scss
+++ b/src/styles/_app.scss
@@ -206,3 +206,13 @@
background-color: color.$paper-shade;
}
}
+
+.account-menu__item--bad {
+ color: color.$bad;
+
+ &:hover,
+ &:focus-visible {
+ color: color.$bad;
+ background-color: rgba(155, 70, 49, 0.09);
+ }
+}
diff --git a/src/styles/_book.scss b/src/styles/_book.scss
index 10feeba..6c4c6f3 100644
--- a/src/styles/_book.scss
+++ b/src/styles/_book.scss
@@ -170,28 +170,40 @@
border-right: 1px solid color.$rule-margin;
}
-/* Where an entry begins, the margin says when it was written */
+/* Where an entry begins, the margin says when it was written.
+ *
+ * Sized off the ruled line like the writing it annotates, so it follows the
+ * hand in use instead of holding one fixed size across six of them - a stamp
+ * in a wide hand at a fixed size is wider than the column it sits in. Both
+ * spans are led at half a rule, so the usual date-over-time comes to exactly
+ * one ruled line and a date long enough to wrap still lands on the book's
+ * rhythm rather than between it. */
.page__stamp {
+ --stamp-size: calc(var(--book-line) * #{var.$hand-scale} * 0.5);
+
position: absolute;
right: 0.7rem;
width: calc(100% - 0.7rem);
text-align: right;
font-family: var.$font-hand;
- line-height: 1.05;
+ line-height: calc(var(--book-line) / 2);
color: color.$ink-soft;
pointer-events: none;
}
.page__stamp-date {
display: block;
- font-size: 1.05rem;
+ font-size: var(--stamp-size);
font-weight: 600;
- white-space: nowrap;
+
+ //A written-out month in a wide hand, or a locale that spells the date out
+ //at length, wraps inside the margin instead of spilling over the page.
+ overflow-wrap: break-word;
}
.page__stamp-time {
display: block;
- font-size: 0.95rem;
+ font-size: calc(var(--stamp-size) * 0.9);
color: color.$ink-faint;
}
diff --git a/src/styles/_mobile.scss b/src/styles/_mobile.scss
index 48e1eea..cf1a4da 100644
--- a/src/styles/_mobile.scss
+++ b/src/styles/_mobile.scss
@@ -77,23 +77,19 @@
}
.page__margin {
- flex-basis: 3.1rem;
+ flex-basis: 4.1rem;
padding-right: 0.4rem;
}
+ //A phone gives the margin less room, so the stamp is written smaller in it
+ //rather than wider than it.
.page__stamp {
+ --stamp-size: calc(var(--book-line) * #{var.$hand-scale} * 0.42);
+
right: 0.4rem;
width: calc(100% - 0.4rem);
}
- .page__stamp-date {
- font-size: 0.9rem;
- }
-
- .page__stamp-time {
- font-size: 0.82rem;
- }
-
.page__column,
.page__measure,
.page__input,
@@ -175,6 +171,41 @@
}
}
+ /* In the drawer there is no page block for a tab to come out from under, so
+ * the + is a card like the tabs are - drawn in the light ink the cover asks
+ * for rather than in paper tones, and it stays put instead of sliding out. */
+ .rail__new {
+ width: 100%;
+ min-height: 2.4rem;
+ margin-left: 0;
+ padding-left: 0;
+ color: rgba(254, 244, 225, 0.6);
+ background-color: rgba(254, 244, 225, 0.06);
+ border: 1px dashed rgba(254, 244, 225, 0.3);
+ border-radius: var.$block-radius;
+
+ &:hover,
+ &:focus-visible {
+ transform: none;
+ color: color.$paper;
+ background-color: rgba(254, 244, 225, 0.12);
+ }
+ }
+
+ .rail__new--current {
+ transform: none;
+ color: color.$paper;
+ background-color: rgba(254, 244, 225, 0.14);
+ border-style: solid;
+ border-color: rgba(254, 244, 225, 0.35);
+ box-shadow: inset -0.2rem 0 0 color.$ribbon;
+
+ &:hover,
+ &:focus-visible {
+ transform: none;
+ }
+ }
+
.calendar {
width: min(19rem, calc(100vw - 1.5rem));
}
diff --git a/src/styles/_rail.scss b/src/styles/_rail.scss
index 17ebc43..dfa112d 100644
--- a/src/styles/_rail.scss
+++ b/src/styles/_rail.scss
@@ -159,6 +159,58 @@
white-space: nowrap;
}
+/* The page nobody has written on yet.
+ *
+ * Shaped like a tab, because that is what it becomes the moment something is
+ * written on it - but a short one, drawn in outline: there is no date to stamp
+ * on it and no day for it to take a dye from. It stands in the stack directly
+ * after the last tab, which is where its own tab will be.
+ */
+.rail__new {
+ flex: 0 0 auto;
+ align-self: flex-start;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 4.75rem;
+ min-height: 1.9rem;
+ //The list's own gap sets it off from the last tab
+ margin-left: calc(-1 * var.$tab-tuck);
+ padding-left: var.$tab-tuck;
+
+ color: color.$ink-faint;
+ background-color: rgba(251, 241, 220, 0.5);
+ //No left edge: like a tab, it comes out from under the page block
+ border: 1px dashed color.$paper-deep;
+ border-left: 0;
+ border-radius: 0 var.$block-radius var.$block-radius 0;
+
+ transition: transform var.$trans-mid ease-out, background-color var.$trans-quick, color var.$trans-quick;
+
+ &:hover,
+ &:focus-visible {
+ transform: translateX(0.45rem);
+ background-color: color.$paper;
+ color: color.$ink-soft;
+ }
+}
+
+/* The book is open at that page, so the + is doing a bookmark's job - and wears
+ * the same ribbon the tab of the page being written on would. */
+.rail__new--current {
+ transform: translateX(0.3rem);
+ color: color.$ink-soft;
+ background-color: color.$paper;
+ border-style: solid;
+ border-color: color.$paper-edge;
+ box-shadow: 0.12rem 0.15rem 0.5rem rgba(90, 54, 31, 0.22), inset -0.2rem 0 0 color.$ribbon;
+
+ &:hover,
+ &:focus-visible {
+ transform: translateX(0.65rem);
+ }
+}
+
/* The rail shows a few tabs either side of where the reading is. These two
* say the book carries on past them, and walk the window along. */
.rail__more {
diff --git a/src/styles/_var.scss b/src/styles/_var.scss
index 75e580f..937a16d 100644
--- a/src/styles/_var.scss
+++ b/src/styles/_var.scss
@@ -14,7 +14,8 @@ $block-spacing: 1rem;
$block-radius: 3px;
//The book
-$page-margin-width: 4.25rem; //the left column that carries date stamps
+$page-margin-width: 5.25rem; //the left column that carries date stamps: wide
+ //enough for a written-out date in the widest hand
$page-padding: 2.25rem;
$spine-width: 2.5rem;
$book-max-width: 72rem; //leaves the rail room to show its dates