Compare commits

..
2 Commits
Author SHA1 Message Date
franzz 1ef95596a2 forgot log files 2026-09-03 18:29:04 +02:00
franzz 361cf93562 v3 init push 2026-09-03 18:28:33 +02:00
40 changed files with 300 additions and 888 deletions
+3 -3
View File
@@ -1,6 +1,6 @@
{ {
"name": "franzz/daydream", "name": "franzz/mythoughts",
"description": "Daydream", "description": "MyThoughts",
"type": "project", "type": "project",
"license": "GPL-3.0-or-later", "license": "GPL-3.0-or-later",
"repositories": [ "repositories": [
@@ -21,7 +21,7 @@
}, },
"autoload": { "autoload": {
"psr-4": { "psr-4": {
"Franzz\\Daydream\\": "lib/", "Franzz\\MyThoughts\\": "lib/",
"Franzz\\Objects\\": "../objects/inc/" "Franzz\\Objects\\": "../objects/inc/"
}, },
"files": [ "files": [
+3 -3
View File
@@ -1,6 +1,6 @@
{ {
"name": "franzz/daydream", "name": "franzz/mythoughts",
"description": "Daydream", "description": "MyThoughts",
"type": "project", "type": "project",
"license": "GPL-3.0-or-later", "license": "GPL-3.0-or-later",
"repositories": [ "repositories": [
@@ -18,7 +18,7 @@
}, },
"autoload": { "autoload": {
"psr-4": { "psr-4": {
"Franzz\\Daydream\\": "lib/" "Franzz\\MyThoughts\\": "lib/"
}, },
"files": [ "files": [
"config/settings.php" "config/settings.php"
+3 -3
View File
@@ -1,12 +1,12 @@
# Serve https://localhost/daydream/ from the public web root. # Serve https://localhost/mythoughts/ from the public web root.
# #
# Include this from the site VirtualHost (or paste the Alias/Directory block # Include this from the site VirtualHost (or paste the Alias/Directory block
# into the existing one). Everything outside public/ - lib/, config/, vendor/, # into the existing one). Everything outside public/ - lib/, config/, vendor/,
# node_modules/ - stays off the document root and is never web-reachable. # node_modules/ - stays off the document root and is never web-reachable.
Alias /daydream /var/www/html/daydream/public Alias /mythoughts /var/www/html/mythoughts/public
<Directory /var/www/html/daydream/public> <Directory /var/www/html/mythoughts/public>
Options FollowSymLinks Options FollowSymLinks
AllowOverride None AllowOverride None
Require all granted Require all granted
+1 -1
View File
@@ -4,7 +4,7 @@ class Settings {
public const DB_SERVER = 'localhost'; public const DB_SERVER = 'localhost';
public const DB_LOGIN = ''; public const DB_LOGIN = '';
public const DB_PASS = ''; public const DB_PASS = '';
public const DB_NAME = 'daydream'; public const DB_NAME = 'mythoughts';
public const DB_ENC = 'utf8mb4'; public const DB_ENC = 'utf8mb4';
public const TEXT_ENC = 'UTF-8'; public const TEXT_ENC = 'UTF-8';
public const TIMEZONE = 'Europe/Zurich'; public const TIMEZONE = 'Europe/Zurich';
+12 -12
View File
@@ -1,6 +1,6 @@
<?php <?php
namespace Franzz\Daydream; namespace Franzz\MyThoughts;
use Franzz\Objects\PhpObject; use Franzz\Objects\PhpObject;
use Franzz\Objects\ToolBox; use Franzz\Objects\ToolBox;
@@ -28,7 +28,7 @@ class Controller extends PhpObject {
'logout' 'logout'
]; ];
private Daydream $oDaydream; private MyThoughts $oMyThoughts;
private array $asReq = []; private array $asReq = [];
private string $sCsrfToken = ''; private string $sCsrfToken = '';
@@ -64,32 +64,32 @@ class Controller extends PhpObject {
//Authentication and CSRF protection share the same server-side session. //Authentication and CSRF protection share the same server-side session.
$this->initCsrfToken(); $this->initCsrfToken();
$this->oDaydream = new Daydream($sProcessPage, $this->asReq['t']); $this->oMyThoughts = new MyThoughts($sProcessPage, $this->asReq['t']);
//Validate CSRF, then release the session lock before long-running work. //Validate CSRF, then release the session lock before long-running work.
$bValidMutationRequest = $this->validateMutationRequest($sAction); $bValidMutationRequest = $this->validateMutationRequest($sAction);
if(!$bValidMutationRequest || !in_array($sAction, self::SESSION_WRITING_ACTIONS, true)) $this->closeSession(); if(!$bValidMutationRequest || !in_array($sAction, self::SESSION_WRITING_ACTIONS, true)) $this->closeSession();
if(!$bValidMutationRequest) $sResult = Daydream::getJsonResult(false, Daydream::UNAUTHORIZED); if(!$bValidMutationRequest) $sResult = MyThoughts::getJsonResult(false, MyThoughts::UNAUTHORIZED);
else $sResult = ($sAction == '') ? $this->oDaydream->getAppMainPage($this->getCsrfToken()) : $this->dispatch($sAction); else $sResult = ($sAction == '') ? $this->oMyThoughts->getAppMainPage($this->getCsrfToken()) : $this->dispatch($sAction);
//Clean errors //Clean errors
$sDebug = ob_get_clean(); $sDebug = ob_get_clean();
if($sDebug != '') $this->oDaydream->addUncaughtError($sDebug); if($sDebug != '') $this->oMyThoughts->addUncaughtError($sDebug);
$this->closeSession(); $this->closeSession();
return $sResult; return $sResult;
} }
private function dispatch(string $sAction): string { private function dispatch(string $sAction): string {
$oJournal = $this->oDaydream->getJournal(); $oJournal = $this->oMyThoughts->getJournal();
return match($sAction) { return match($sAction) {
/* Account */ /* Account */
'signup' => $this->oDaydream->signup($this->asReq['name'], $this->asReq['email'], $this->asReq['password'], $this->asReq['t']), 'signup' => $this->oMyThoughts->signup($this->asReq['name'], $this->asReq['email'], $this->asReq['password'], $this->asReq['t']),
'login' => $this->oDaydream->login($this->asReq['email'], $this->asReq['password'], $this->asReq['t'], $this->asReq['remember']), 'login' => $this->oMyThoughts->login($this->asReq['email'], $this->asReq['password'], $this->asReq['t'], $this->asReq['remember']),
'logout' => $this->oDaydream->logout(), 'logout' => $this->oMyThoughts->logout(),
'account' => $this->oDaydream->updateAccount($this->asReq['field'], $this->asReq['value']), 'account' => $this->oMyThoughts->updateAccount($this->asReq['field'], $this->asReq['value']),
/* Reading the book */ /* Reading the book */
'book' => $oJournal->getBook(), 'book' => $oJournal->getBook(),
@@ -102,7 +102,7 @@ class Controller extends PhpObject {
'close_entry' => $oJournal->closeEntry($this->asReq['id'], $this->asReq['content'], $this->asReq['has_content']), 'close_entry' => $oJournal->closeEntry($this->asReq['id'], $this->asReq['content'], $this->asReq['has_content']),
'delete_entry' => $oJournal->deleteEntry($this->asReq['id']), 'delete_entry' => $oJournal->deleteEntry($this->asReq['id']),
default => Daydream::getJsonResult(false, Daydream::NOT_FOUND) default => MyThoughts::getJsonResult(false, MyThoughts::NOT_FOUND)
}; };
} }
+29 -31
View File
@@ -1,6 +1,6 @@
<?php <?php
namespace Franzz\Daydream; namespace Franzz\MyThoughts;
use Franzz\Objects\Db; use Franzz\Objects\Db;
use Franzz\Objects\PhpObject; use Franzz\Objects\PhpObject;
@@ -47,13 +47,13 @@ class Journal extends PhpObject {
* the calendar. Bookmarks stay cheap - id and timestamp only, no content. * the calendar. Bookmarks stay cheap - id and timestamp only, no content.
*/ */
public function getBook(): string { public function getBook(): string {
if(!$this->oUser->isLoggedIn()) return Daydream::getJsonResult(false, Daydream::UNAUTHORIZED); if(!$this->oUser->isLoggedIn()) return MyThoughts::getJsonResult(false, MyThoughts::UNAUTHORIZED);
$this->sealStaleEntries(); $this->sealStaleEntries();
$asEntries = $this->getEntryWindow(); $asEntries = $this->getEntryWindow();
return Daydream::getJsonResult(true, '', [ return MyThoughts::getJsonResult(true, '', [
'entries' => $asEntries, 'entries' => $asEntries,
'bookmarks' => $this->getBookmarks(), 'bookmarks' => $this->getBookmarks(),
'has_older' => $this->hasOlderThan($asEntries[0]['id'] ?? 0), 'has_older' => $this->hasOlderThan($asEntries[0]['id'] ?? 0),
@@ -62,7 +62,7 @@ class Journal extends PhpObject {
} }
public function getEntries(string $sDirection, int $iCursorId): string { public function getEntries(string $sDirection, int $iCursorId): string {
if(!$this->oUser->isLoggedIn()) return Daydream::getJsonResult(false, Daydream::UNAUTHORIZED); if(!$this->oUser->isLoggedIn()) return MyThoughts::getJsonResult(false, MyThoughts::UNAUTHORIZED);
$asEntries = match($sDirection) { $asEntries = match($sDirection) {
'before' => $this->getEntryWindow($iCursorId, 'before'), 'before' => $this->getEntryWindow($iCursorId, 'before'),
@@ -71,9 +71,9 @@ class Journal extends PhpObject {
default => null default => null
}; };
if($asEntries === null) return Daydream::getJsonResult(false, Daydream::NOT_FOUND); if($asEntries === null) return MyThoughts::getJsonResult(false, MyThoughts::NOT_FOUND);
return Daydream::getJsonResult(true, '', [ return MyThoughts::getJsonResult(true, '', [
'entries' => $asEntries, 'entries' => $asEntries,
'has_older' => $this->hasOlderThan($asEntries[0]['id'] ?? 0), 'has_older' => $this->hasOlderThan($asEntries[0]['id'] ?? 0),
'has_newer' => $this->hasNewerThan(end($asEntries)['id'] ?? 0) 'has_newer' => $this->hasNewerThan(end($asEntries)['id'] ?? 0)
@@ -86,18 +86,18 @@ class Journal extends PhpObject {
* everything after it is blank. * everything after it is blank.
*/ */
public function getEntryIdAtDate(string $sDate): string { public function getEntryIdAtDate(string $sDate): string {
if(!$this->oUser->isLoggedIn()) return Daydream::getJsonResult(false, Daydream::UNAUTHORIZED); if(!$this->oUser->isLoggedIn()) return MyThoughts::getJsonResult(false, MyThoughts::UNAUTHORIZED);
$oDate = \DateTime::createFromFormat('Y-m-d', $sDate); $oDate = \DateTime::createFromFormat('Y-m-d', $sDate);
if(!$oDate) return Daydream::getJsonResult(false, Daydream::NOT_FOUND); if(!$oDate) return MyThoughts::getJsonResult(false, MyThoughts::NOT_FOUND);
$sDate = $oDate->format('Y-m-d'); $sDate = $oDate->format('Y-m-d');
$sMidnight = $sDate.' 00:00:00'; $sMidnight = $sDate.' 00:00:00';
$iEntryId = $this->selectFirstId(['started_on' => $sMidnight], ['started_on' => ' >= '], 'ASC'); $iEntryId = $this->selectFirstId(['started_on' => $sMidnight], ['started_on' => ' >= '], 'ASC');
if(!$iEntryId) $iEntryId = $this->selectFirstId(['started_on' => $sMidnight], ['started_on' => ' < '], 'DESC'); if(!$iEntryId) $iEntryId = $this->selectFirstId(['started_on' => $sMidnight], ['started_on' => ' < '], 'DESC');
if(!$iEntryId) return Daydream::getJsonResult(false, 'book.no_entry_yet'); if(!$iEntryId) return MyThoughts::getJsonResult(false, 'book.no_entry_yet');
return Daydream::getJsonResult(true, '', ['id' => (int) $iEntryId]); return MyThoughts::getJsonResult(true, '', ['id' => (int) $iEntryId]);
} }
/* Writing */ /* Writing */
@@ -107,7 +107,7 @@ class Journal extends PhpObject {
* open from a reload a minute ago, or a fresh one stamped with now. * open from a reload a minute ago, or a fresh one stamped with now.
*/ */
public function openEntry(): string { public function openEntry(): string {
if(!$this->oUser->isLoggedIn()) return Daydream::getJsonResult(false, Daydream::UNAUTHORIZED); if(!$this->oUser->isLoggedIn()) return MyThoughts::getJsonResult(false, MyThoughts::UNAUTHORIZED);
$this->sealStaleEntries(); $this->sealStaleEntries();
@@ -119,27 +119,27 @@ class Journal extends PhpObject {
'content' => '', 'content' => '',
'status' => self::STATUS_OPEN, 'status' => self::STATUS_OPEN,
'started_on' => date(Db::TIMESTAMP_FORMAT), 'started_on' => date(Db::TIMESTAMP_FORMAT),
'closed_on' => Daydream::ZERO_TIMESTAMP, 'closed_on' => MyThoughts::ZERO_TIMESTAMP,
'timezone' => date_default_timezone_get() 'timezone' => date_default_timezone_get()
]); ]);
if($iEntryId <= 0) return Daydream::getJsonResult(false, 'error.commit_db'); if($iEntryId <= 0) return MyThoughts::getJsonResult(false, 'error.commit_db');
} }
return Daydream::getJsonResult(true, '', ['entry' => $this->getEntryById($iEntryId)]); return MyThoughts::getJsonResult(true, '', ['entry' => $this->getEntryById($iEntryId)]);
} }
public function saveEntry(int $iEntryId, string $sContent): string { public function saveEntry(int $iEntryId, string $sContent): string {
if(!$this->oUser->isLoggedIn()) return Daydream::getJsonResult(false, Daydream::UNAUTHORIZED); if(!$this->oUser->isLoggedIn()) return MyThoughts::getJsonResult(false, MyThoughts::UNAUTHORIZED);
if(!$this->ownsEntry($iEntryId)) return Daydream::getJsonResult(false, Daydream::NOT_FOUND); if(!$this->ownsEntry($iEntryId)) return MyThoughts::getJsonResult(false, MyThoughts::NOT_FOUND);
$sContent = self::normaliseContent($sContent); $sContent = self::normaliseContent($sContent);
if($this->oDb->updateRow(self::ENTRY_TABLE, $iEntryId, ['content' => $sContent]) === false) { if($this->oDb->updateRow(self::ENTRY_TABLE, $iEntryId, ['content' => $sContent]) === false) {
return Daydream::getJsonResult(false, 'error.commit_db'); return MyThoughts::getJsonResult(false, 'error.commit_db');
} }
return Daydream::getJsonResult(true, '', ['id' => $iEntryId, 'saved_at' => time()]); return MyThoughts::getJsonResult(true, '', ['id' => $iEntryId, 'saved_at' => time()]);
} }
/** /**
@@ -147,11 +147,11 @@ class Journal extends PhpObject {
* nobody actually wrote in is dropped rather than left as a blank page. * nobody actually wrote in is dropped rather than left as a blank page.
*/ */
public function closeEntry(int $iEntryId, string $sContent = '', bool $bHasContent = false): string { public function closeEntry(int $iEntryId, string $sContent = '', bool $bHasContent = false): string {
if(!$this->oUser->isLoggedIn()) return Daydream::getJsonResult(false, Daydream::UNAUTHORIZED); if(!$this->oUser->isLoggedIn()) return MyThoughts::getJsonResult(false, MyThoughts::UNAUTHORIZED);
if(!$this->ownsEntry($iEntryId)) return Daydream::getJsonResult(false, Daydream::NOT_FOUND); if(!$this->ownsEntry($iEntryId)) return MyThoughts::getJsonResult(false, MyThoughts::NOT_FOUND);
$asResult = $this->sealEntry($iEntryId, $bHasContent ? $sContent : null); $asResult = $this->sealEntry($iEntryId, $bHasContent ? $sContent : null);
return Daydream::getJsonResult($asResult['result'], $asResult['desc_lang_id'], $asResult['data']); return MyThoughts::getJsonResult($asResult['result'], $asResult['desc_lang_id'], $asResult['data']);
} }
/** /**
@@ -169,23 +169,23 @@ class Journal extends PhpObject {
if(trim($sStored) === '') { if(trim($sStored) === '') {
$this->oDb->deleteRow(self::ENTRY_TABLE, $iEntryId); $this->oDb->deleteRow(self::ENTRY_TABLE, $iEntryId);
return Daydream::getResult(true, '', ['id' => $iEntryId, 'discarded' => true]); return MyThoughts::getResult(true, '', ['id' => $iEntryId, 'discarded' => true]);
} }
if($this->oDb->updateRow(self::ENTRY_TABLE, $iEntryId, $asData) === false) { if($this->oDb->updateRow(self::ENTRY_TABLE, $iEntryId, $asData) === false) {
return Daydream::getResult(false, 'error.commit_db'); return MyThoughts::getResult(false, 'error.commit_db');
} }
return Daydream::getResult(true, '', ['entry' => $this->getEntryById($iEntryId), 'discarded' => false]); return MyThoughts::getResult(true, '', ['entry' => $this->getEntryById($iEntryId), 'discarded' => false]);
} }
public function deleteEntry(int $iEntryId): string { public function deleteEntry(int $iEntryId): string {
if(!$this->oUser->isLoggedIn()) return Daydream::getJsonResult(false, Daydream::UNAUTHORIZED); if(!$this->oUser->isLoggedIn()) return MyThoughts::getJsonResult(false, MyThoughts::UNAUTHORIZED);
if(!$this->ownsEntry($iEntryId)) return Daydream::getJsonResult(false, Daydream::NOT_FOUND); if(!$this->ownsEntry($iEntryId)) return MyThoughts::getJsonResult(false, MyThoughts::NOT_FOUND);
if(!$this->oDb->deleteRow(self::ENTRY_TABLE, $iEntryId)) return Daydream::getJsonResult(false, 'error.commit_db'); if(!$this->oDb->deleteRow(self::ENTRY_TABLE, $iEntryId)) return MyThoughts::getJsonResult(false, 'error.commit_db');
return Daydream::getJsonResult(true, 'book.entry_deleted', ['id' => $iEntryId]); return MyThoughts::getJsonResult(true, 'book.entry_deleted', ['id' => $iEntryId]);
} }
/* Internals */ /* Internals */
@@ -265,9 +265,7 @@ class Journal extends PhpObject {
], ],
'from' => self::ENTRY_TABLE, 'from' => self::ENTRY_TABLE,
'constraint'=> [Db::getId(User::USER_TABLE) => $this->oUser->getUserId()], 'constraint'=> [Db::getId(User::USER_TABLE) => $this->oUser->getUserId()],
//By when it was written, not by when it was inserted. The id is only 'orderBy' => [$sIdColumn => 'ASC']
//the tiebreaker for two entries begun in the same second.
'orderBy' => ['started_on' => 'ASC', $sIdColumn => 'ASC']
]); ]);
return array_map(static function(array $asRow): array { return array_map(static function(array $asRow): array {
+4 -35
View File
@@ -1,6 +1,6 @@
<?php <?php
namespace Franzz\Daydream; namespace Franzz\MyThoughts;
use Franzz\Objects\Db; use Franzz\Objects\Db;
use Franzz\Objects\Main; use Franzz\Objects\Main;
@@ -16,39 +16,10 @@ use Settings;
* page stamped with the local time of the moment it was written - and the * page stamped with the local time of the moment it was written - and the
* client formats from a UNIX timestamp, never from a preformatted string. * client formats from a UNIX timestamp, never from a preformatted string.
*/ */
class Daydream extends Main { class MyThoughts extends Main {
public const PROJECT_NAME = 'Daydream'; public const PROJECT_NAME = 'MyThoughts';
public const DEFAULT_LANG = 'en'; public const DEFAULT_LANG = 'en';
/**
* The hands the book can be written in.
*
* One list, here, because two things need it and they must not drift: the
* settings picker offers it, and updateSettings() validates against it.
*
* `scale` is the type size as a fraction of one ruled line. It is not
* cosmetic - every one of these hands has a different x-height for the same
* em, so a single size would leave one sitting on the rule and the next
* floating above it. The values are measured, not chosen.
*
* Adding one here also needs its webfont imported in src/scripts/fonts.js.
*/
public const HANDS = [
['id' => 'caveat', 'name' => 'Caveat', 'family' => '"Caveat Variable", "Caveat"', 'scale' => 0.82],
['id' => 'kalam', 'name' => 'Kalam', 'family' => '"Kalam"', 'scale' => 0.81],
['id' => 'patrick', 'name' => 'Patrick Hand', 'family' => '"Patrick Hand"', 'scale' => 0.8],
['id' => 'shadows', 'name' => 'Shadows Into Light', 'family' => '"Shadows Into Light"', 'scale' => 0.7],
['id' => 'architect', 'name' => "Architect's Daughter",'family' => '"Architects Daughter"', 'scale' => 0.68],
['id' => 'indie', 'name' => 'Indie Flower', 'family' => '"Indie Flower"', 'scale' => 0.67]
];
public const DEFAULT_HAND = 'caveat';
/** @return string[] ids of every hand on offer */
public static function getHandIds(): array {
return array_column(self::HANDS, 'id');
}
//The dictionaries shipped in resources/lang. Listed rather than globbed: //The dictionaries shipped in resources/lang. Listed rather than globbed:
//Translator resolves its folder relative to the calling script, and the //Translator resolves its folder relative to the calling script, and the
//settings panel needs the list on a page load either way. //settings panel needs the list on a page load either way.
@@ -91,7 +62,7 @@ class Daydream extends Main {
protected function getSqlOptions() { protected function getSqlOptions() {
return [ return [
'tables' => [ 'tables' => [
User::USER_TABLE => ['name', 'email', 'password', 'token', 'token_exp', 'language', 'timezone', 'hand', 'clearance'], User::USER_TABLE => ['name', 'email', 'password', 'token', 'token_exp', 'language', 'timezone', 'clearance'],
Journal::ENTRY_TABLE=> [Db::getId(User::USER_TABLE), 'content', 'status', 'started_on', 'closed_on', 'timezone'] Journal::ENTRY_TABLE=> [Db::getId(User::USER_TABLE), 'content', 'status', 'started_on', 'closed_on', 'timezone']
], ],
'types' => [ 'types' => [
@@ -100,7 +71,6 @@ class Daydream extends Main {
'password' => "VARCHAR(255) NOT NULL DEFAULT ''", 'password' => "VARCHAR(255) NOT NULL DEFAULT ''",
'token' => "VARCHAR(64) NOT NULL DEFAULT ''", 'token' => "VARCHAR(64) NOT NULL DEFAULT ''",
'token_exp' => 'TIMESTAMP DEFAULT 0', 'token_exp' => 'TIMESTAMP DEFAULT 0',
'hand' => "VARCHAR(20) NOT NULL DEFAULT '".self::DEFAULT_HAND."'",
'language' => 'VARCHAR(2)', 'language' => 'VARCHAR(2)',
'timezone' => 'CHAR(64) NOT NULL', //see mysql.time_zone_name 'timezone' => 'CHAR(64) NOT NULL', //see mysql.time_zone_name
'clearance' => 'TINYINT(1) DEFAULT '.User::CLEARANCE_USER, 'clearance' => 'TINYINT(1) DEFAULT '.User::CLEARANCE_USER,
@@ -132,7 +102,6 @@ class Daydream extends Main {
'consts' => [ 'consts' => [
'title' => self::PROJECT_NAME, 'title' => self::PROJECT_NAME,
'languages' => self::LANGUAGES, 'languages' => self::LANGUAGES,
'hands' => self::HANDS,
'chunk_size' => Journal::CHUNK_SIZE, 'chunk_size' => Journal::CHUNK_SIZE,
'default_timezone' => Settings::TIMEZONE, 'default_timezone' => Settings::TIMEZONE,
'autosave_delay' => 1200, //ms of stillness before a save 'autosave_delay' => 1200, //ms of stillness before a save
+18 -21
View File
@@ -1,6 +1,6 @@
<?php <?php
namespace Franzz\Daydream; namespace Franzz\MyThoughts;
use Franzz\Objects\Db; use Franzz\Objects\Db;
use Franzz\Objects\PhpObject; use Franzz\Objects\PhpObject;
@@ -26,13 +26,12 @@ class User extends PhpObject {
'email' => '', 'email' => '',
'language' => '', 'language' => '',
'timezone' => '', 'timezone' => '',
'hand' => Daydream::DEFAULT_HAND,
'clearance' => self::CLEARANCE_USER 'clearance' => self::CLEARANCE_USER
]; ];
//Session & Cookie //Session & Cookie
private const SESSION_ID_USER = 'id_user'; private const SESSION_ID_USER = 'id_user';
private const COOKIE_TOKEN = 'daydream'; private const COOKIE_TOKEN = 'mythoughts';
private const COOKIE_DURATION = 60 * 60 * 24 * 90; //3 months private const COOKIE_DURATION = 60 * 60 * 24 * 90; //3 months
private Db $oDb; private Db $oDb;
@@ -101,14 +100,14 @@ class User extends PhpObject {
$sEmail = mb_strtolower(trim($sEmail)); $sEmail = mb_strtolower(trim($sEmail));
$sName = mb_substr(trim($sName), 0, self::MAX_NAME_LENGTH); $sName = mb_substr(trim($sName), 0, self::MAX_NAME_LENGTH);
if($sName === '') return Daydream::getResult(false, 'account.name_required'); if($sName === '') return MyThoughts::getResult(false, 'account.name_required');
if(!filter_var($sEmail, FILTER_VALIDATE_EMAIL)) return Daydream::getResult(false, 'account.invalid_email'); if(!filter_var($sEmail, FILTER_VALIDATE_EMAIL)) return MyThoughts::getResult(false, 'account.invalid_email');
if(mb_strlen($sPassword) < self::MIN_PASSWORD_LENGTH) return Daydream::getResult(false, 'account.password_too_short', [], [self::MIN_PASSWORD_LENGTH]); if(mb_strlen($sPassword) < self::MIN_PASSWORD_LENGTH) return MyThoughts::getResult(false, 'account.password_too_short', [], [self::MIN_PASSWORD_LENGTH]);
//Taken emails must not be distinguishable from a wrong password, so the //Taken emails must not be distinguishable from a wrong password, so the
//message stays the same one login gives - no account enumeration here. //message stays the same one login gives - no account enumeration here.
if($this->oDb->selectId(self::USER_TABLE, ['email' => $sEmail]) > 0) { if($this->oDb->selectId(self::USER_TABLE, ['email' => $sEmail]) > 0) {
return Daydream::getResult(false, 'account.invalid_credentials'); return MyThoughts::getResult(false, 'account.invalid_credentials');
} }
$iUserId = $this->oDb->insertRow(self::USER_TABLE, [ $iUserId = $this->oDb->insertRow(self::USER_TABLE, [
@@ -117,14 +116,13 @@ class User extends PhpObject {
'password' => password_hash($sPassword, PASSWORD_DEFAULT), 'password' => password_hash($sPassword, PASSWORD_DEFAULT),
'language' => $sLang, 'language' => $sLang,
'timezone' => $sTimezone, 'timezone' => $sTimezone,
'hand' => Daydream::DEFAULT_HAND,
'clearance' => self::CLEARANCE_USER 'clearance' => self::CLEARANCE_USER
]); ]);
if($iUserId <= 0) return Daydream::getResult(false, 'error.commit_db'); if($iUserId <= 0) return MyThoughts::getResult(false, 'error.commit_db');
$this->openSessionFor($iUserId, true); $this->openSessionFor($iUserId, true);
return Daydream::getResult(true, 'account.welcome', ['user' => $this->getUserInfo()]); return MyThoughts::getResult(true, 'account.welcome', ['user' => $this->getUserInfo()]);
} }
public function login(string $sEmail, string $sPassword, string $sTimezone, bool $bRemember): array { public function login(string $sEmail, string $sPassword, string $sTimezone, bool $bRemember): array {
@@ -142,14 +140,14 @@ class User extends PhpObject {
$sHash = $asDbUser['password'] ?? ''; $sHash = $asDbUser['password'] ?? '';
$bValid = ($sHash !== '') ? password_verify($sPassword, $sHash) : password_verify($sPassword, '$2y$10$'.str_repeat('.', 53)); $bValid = ($sHash !== '') ? password_verify($sPassword, $sHash) : password_verify($sPassword, '$2y$10$'.str_repeat('.', 53));
if($iUserId <= 0 || !$bValid) return Daydream::getResult(false, 'account.invalid_credentials'); if($iUserId <= 0 || !$bValid) return MyThoughts::getResult(false, 'account.invalid_credentials');
if($sTimezone !== '' && $sTimezone !== ($asDbUser['timezone'] ?? '')) { if($sTimezone !== '' && $sTimezone !== ($asDbUser['timezone'] ?? '')) {
$this->oDb->updateRow(self::USER_TABLE, $iUserId, ['timezone' => $sTimezone]); $this->oDb->updateRow(self::USER_TABLE, $iUserId, ['timezone' => $sTimezone]);
} }
$this->openSessionFor($iUserId, $bRemember); $this->openSessionFor($iUserId, $bRemember);
return Daydream::getResult(true, 'account.logged_in', ['user' => $this->getUserInfo()]); return MyThoughts::getResult(true, 'account.logged_in', ['user' => $this->getUserInfo()]);
} }
public function logout(): array { public function logout(): array {
@@ -159,24 +157,23 @@ class User extends PhpObject {
if(session_status() === PHP_SESSION_ACTIVE) session_regenerate_id(true); if(session_status() === PHP_SESSION_ACTIVE) session_regenerate_id(true);
$this->setUserId(0); $this->setUserId(0);
return Daydream::getResult(true, 'account.logged_out'); return MyThoughts::getResult(true, 'account.logged_out');
} }
public function updateSettings(string $sField, string $sValue): array { public function updateSettings(string $sField, string $sValue): array {
if(!$this->isLoggedIn()) return Daydream::getResult(false, Daydream::UNAUTHORIZED); if(!$this->isLoggedIn()) return MyThoughts::getResult(false, MyThoughts::UNAUTHORIZED);
if(!in_array($sField, ['name', 'language', 'timezone', 'hand'], true)) return Daydream::getResult(false, Daydream::NOT_FOUND); if(!in_array($sField, ['name', 'language', 'timezone'], true)) return MyThoughts::getResult(false, MyThoughts::NOT_FOUND);
$sValue = mb_substr(trim($sValue), 0, self::MAX_NAME_LENGTH); $sValue = mb_substr(trim($sValue), 0, self::MAX_NAME_LENGTH);
if($sField === 'name' && $sValue === '') return Daydream::getResult(false, 'account.name_required'); if($sField === 'name' && $sValue === '') return MyThoughts::getResult(false, 'account.name_required');
if($sField === 'timezone' && !in_array($sValue, \DateTimeZone::listIdentifiers(), true)) return Daydream::getResult(false, Daydream::NOT_FOUND); if($sField === 'timezone' && !in_array($sValue, \DateTimeZone::listIdentifiers(), true)) return MyThoughts::getResult(false, MyThoughts::NOT_FOUND);
if($sField === 'hand' && !in_array($sValue, Daydream::getHandIds(), true)) return Daydream::getResult(false, Daydream::NOT_FOUND);
if(!$this->oDb->updateRow(self::USER_TABLE, $this->iUserId, [$sField => $sValue])) { if(!$this->oDb->updateRow(self::USER_TABLE, $this->iUserId, [$sField => $sValue])) {
return Daydream::getResult(false, 'error.commit_db'); return MyThoughts::getResult(false, 'error.commit_db');
} }
$this->setUserId($this->iUserId); $this->setUserId($this->iUserId);
return Daydream::getResult(true, 'account.saved', ['user' => $this->getUserInfo()]); return MyThoughts::getResult(true, 'account.saved', ['user' => $this->getUserInfo()]);
} }
/* Session plumbing */ /* Session plumbing */
@@ -255,7 +252,7 @@ class User extends PhpObject {
private function clearTokenCookie(): void { private function clearTokenCookie(): void {
if($this->isLoggedIn()) { if($this->isLoggedIn()) {
$this->oDb->updateRow(self::USER_TABLE, $this->iUserId, ['token' => '', 'token_exp' => Daydream::ZERO_TIMESTAMP]); $this->oDb->updateRow(self::USER_TABLE, $this->iUserId, ['token' => '', 'token_exp' => MyThoughts::ZERO_TIMESTAMP]);
} }
$this->writeCookie('', time() - 3600); $this->writeCookie('', time() - 3600);
+2 -52
View File
@@ -1,19 +1,14 @@
{ {
"name": "daydream", "name": "mythoughts",
"version": "2.0.0", "version": "2.0.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "daydream", "name": "mythoughts",
"version": "2.0.0", "version": "2.0.0",
"dependencies": { "dependencies": {
"@fontsource-variable/caveat": "^5.3.0", "@fontsource-variable/caveat": "^5.3.0",
"@fontsource/architects-daughter": "^5.3.0",
"@fontsource/indie-flower": "^5.3.0",
"@fontsource/kalam": "^5.3.0",
"@fontsource/patrick-hand": "^5.3.0",
"@fontsource/shadows-into-light": "^5.3.0",
"sass": "^1.103.1", "sass": "^1.103.1",
"vue": "^3.5.42" "vue": "^3.5.42"
}, },
@@ -209,51 +204,6 @@
"url": "https://github.com/sponsors/ayuhito" "url": "https://github.com/sponsors/ayuhito"
} }
}, },
"node_modules/@fontsource/architects-daughter": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/@fontsource/architects-daughter/-/architects-daughter-5.3.0.tgz",
"integrity": "sha512-uezOgAlbhmf9nDW69VYq31lXNA+0uRFWXBGCSFWLXVcrNESFHpNjvu11kJss1PMsfH/YKz1hnnYB+JSNI/t+ew==",
"license": "OFL-1.1",
"funding": {
"url": "https://github.com/sponsors/ayuhito"
}
},
"node_modules/@fontsource/indie-flower": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/@fontsource/indie-flower/-/indie-flower-5.3.0.tgz",
"integrity": "sha512-Wo5eN/ruEQcyC3+ArzhlpqWQgph3VuCNbJlEkVw17LUd0uyTHOb9bTec4g9njQzjiL/ijACF27sSwy90L6kZBg==",
"license": "OFL-1.1",
"funding": {
"url": "https://github.com/sponsors/ayuhito"
}
},
"node_modules/@fontsource/kalam": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/@fontsource/kalam/-/kalam-5.3.0.tgz",
"integrity": "sha512-FDkVoDfPDCSN/eO81FOYtUI67eMgpVBJmjBtAqSL0PmEhJyeFY2biC+CR+yEttksgXR9+swpFdYmQTyjwBq80Q==",
"license": "OFL-1.1",
"funding": {
"url": "https://github.com/sponsors/ayuhito"
}
},
"node_modules/@fontsource/patrick-hand": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/@fontsource/patrick-hand/-/patrick-hand-5.3.0.tgz",
"integrity": "sha512-V8C38IlIFfdWg0Xtri3F7sLCVpm069rUwn6w4n+oyn38gt7H4arOJmuReg3tAw1Q6jHqZp89wTnAAJagq4Atxw==",
"license": "OFL-1.1",
"funding": {
"url": "https://github.com/sponsors/ayuhito"
}
},
"node_modules/@fontsource/shadows-into-light": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/@fontsource/shadows-into-light/-/shadows-into-light-5.3.0.tgz",
"integrity": "sha512-3dvv0i8/ZwLMY08AAlTLivHUdNQYB6Fxe92d7BZiGbHY2GQnCxEd8JC9CBzxTiP4iyWNa6OZn9LXF+xjc9LaGw==",
"license": "OFL-1.1",
"funding": {
"url": "https://github.com/sponsors/ayuhito"
}
},
"node_modules/@humanfs/core": { "node_modules/@humanfs/core": {
"version": "0.19.2", "version": "0.19.2",
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz",
+2 -7
View File
@@ -1,6 +1,6 @@
{ {
"name": "daydream", "name": "mythoughts",
"description": "a place for wandering thoughts.", "description": "A journal that behaves like an open book: write the left page, then the right, then turn.",
"version": "2.0.0", "version": "2.0.0",
"private": true, "private": true,
"type": "module", "type": "module",
@@ -12,11 +12,6 @@
"author": "Franzz", "author": "Franzz",
"dependencies": { "dependencies": {
"@fontsource-variable/caveat": "^5.3.0", "@fontsource-variable/caveat": "^5.3.0",
"@fontsource/architects-daughter": "^5.3.0",
"@fontsource/indie-flower": "^5.3.0",
"@fontsource/kalam": "^5.3.0",
"@fontsource/patrick-hand": "^5.3.0",
"@fontsource/shadows-into-light": "^5.3.0",
"sass": "^1.103.1", "sass": "^1.103.1",
"vue": "^3.5.42" "vue": "^3.5.42"
}, },
+1 -1
View File
@@ -2,6 +2,6 @@
require __DIR__.'/../vendor/autoload.php'; require __DIR__.'/../vendor/autoload.php';
use Franzz\Daydream\Controller; use Franzz\MyThoughts\Controller;
echo (new Controller())->handle(__FILE__, $argv ?? []); echo (new Controller())->handle(__FILE__, $argv ?? []);
+4 -8
View File
@@ -31,13 +31,11 @@
"saved": "Saved.", "saved": "Saved.",
"settings": "Settings", "settings": "Settings",
"language": "Language", "language": "Language",
"timezone": "Time zone", "timezone": "Time zone"
"hand": "Handwriting",
"hand_sample": "The morning came"
}, },
"book": { "book": {
"title": "Daydream", "title": "MyThoughts",
"tagline": "a place for wandering thoughts.", "tagline": "Your thoughts, on paper.",
"write_here": "Write here…", "write_here": "Write here…",
"first_page": "This is the first page of your book.", "first_page": "This is the first page of your book.",
"no_entry_yet": "Nothing was written around that date.", "no_entry_yet": "Nothing was written around that date.",
@@ -47,9 +45,7 @@
"loading": "Turning pages…", "loading": "Turning pages…",
"today": "Today", "today": "Today",
"go_to_writing": "Back to today's page", "go_to_writing": "Back to today's page",
"empty_entry": "Blank page", "empty_entry": "Blank page"
"earlier": "Earlier entries",
"later": "Later entries"
}, },
"action": { "action": {
"prev_page": "Previous page", "prev_page": "Previous page",
+4 -8
View File
@@ -31,13 +31,11 @@
"saved": "Enregistré.", "saved": "Enregistré.",
"settings": "Réglages", "settings": "Réglages",
"language": "Langue", "language": "Langue",
"timezone": "Fuseau horaire", "timezone": "Fuseau horaire"
"hand": "Écriture",
"hand_sample": "Le matin est venu"
}, },
"book": { "book": {
"title": "Daydream", "title": "MyThoughts",
"tagline": "Où l'esprit vagabonde.", "tagline": "Vos pensées, sur le papier.",
"write_here": "Écrivez ici…", "write_here": "Écrivez ici…",
"first_page": "C'est la première page de votre carnet.", "first_page": "C'est la première page de votre carnet.",
"no_entry_yet": "Rien n'a été écrit autour de cette date.", "no_entry_yet": "Rien n'a été écrit autour de cette date.",
@@ -47,9 +45,7 @@
"loading": "On tourne les pages…", "loading": "On tourne les pages…",
"today": "Aujourd'hui", "today": "Aujourd'hui",
"go_to_writing": "Revenir à la page du jour", "go_to_writing": "Revenir à la page du jour",
"empty_entry": "Page blanche", "empty_entry": "Page blanche"
"earlier": "Entrées précédentes",
"later": "Entrées suivantes"
}, },
"action": { "action": {
"prev_page": "Page précédente", "prev_page": "Page précédente",
+3 -3
View File
@@ -6,10 +6,10 @@
<meta name="description" content="[#]lang:meta.page_desc[#]"> <meta name="description" content="[#]lang:meta.page_desc[#]">
<meta name="robots" content="noindex, nofollow"> <meta name="robots" content="noindex, nofollow">
<meta name="color-scheme" content="light"> <meta name="color-scheme" content="light">
<meta name="theme-color" content="#fef9ed"> <meta name="theme-color" content="#2c2114">
<meta name="apple-mobile-web-app-title" content="[#]title[#]" /> <meta name="apple-mobile-web-app-title" content="[#]title[#]" />
<link rel="icon" type="image/png" href="assets/images/icons/favicon.png" /> <link rel="icon" type="image/svg+xml" href="assets/images/icons/favicon.svg" />
<link rel="apple-touch-icon" href="assets/images/icons/apple-touch-icon.png" /> <link rel="apple-touch-icon" href="assets/images/icons/apple-touch-icon.svg" />
<link rel="manifest" href="assets/images/icons/site.webmanifest" /> <link rel="manifest" href="assets/images/icons/site.webmanifest" />
<script id="app-config" type="application/json">[#]app_config[#]</script> <script id="app-config" type="application/json">[#]app_config[#]</script>
<title>[#]title[#]</title> <title>[#]title[#]</title>
+2 -6
View File
@@ -184,12 +184,8 @@ export default {
<div class="app"> <div class="app">
<header class="app__header"> <header class="app__header">
<h1 class="app__brand"> <h1 class="app__brand">
<img class="app__logo" :src="sLogo" alt="" /> <img class="app__logo" :src="sLogo" :alt="consts.title" />
<span class="app__name"> <span class="app__tagline">{{ lang.get('book.tagline') }}</span>
<!-- The app's own name, in the app's own handwriting -->
<span class="app__wordmark">{{ consts.title }}</span>
<span class="app__tagline">{{ lang.get('book.tagline') }}</span>
</span>
</h1> </h1>
<div class="app__tools"> <div class="app__tools">
+1 -6
View File
@@ -1,6 +1,5 @@
//Librairies //Librairies
import 'vite/modulepreload-polyfill'; import 'vite/modulepreload-polyfill';
import { applyHand } from '@scripts/hands';
import Api from '@scripts/api'; import Api from '@scripts/api';
import Journal from '@scripts/journal'; import Journal from '@scripts/journal';
import Lang from '@scripts/lang'; import Lang from '@scripts/lang';
@@ -11,7 +10,7 @@ import { createApp, reactive } from 'vue';
import App from './App.vue'; import App from './App.vue';
//Style //Style
import '@styles/daydream.scss'; import '@styles/mythoughts.scss';
//App Configuration from PHP //App Configuration from PHP
const appConfig = JSON.parse(document.getElementById('app-config').textContent); const appConfig = JSON.parse(document.getElementById('app-config').textContent);
@@ -37,10 +36,6 @@ const oApi = new Api({
}); });
const oUser = reactive({...appConfig.user}); const oUser = reactive({...appConfig.user});
//Before the first paint, so the book is never drawn in one hand and then
//redrawn in another
applyHand(appConfig.consts.hands, oUser.hand);
const oJournal = reactive(new Journal(oApi, appConfig.consts)); const oJournal = reactive(new Journal(oApi, appConfig.consts));
//Mount app //Mount app
+2 -2
View File
@@ -84,8 +84,8 @@ export default {
<div class="veil"> <div class="veil">
<form class="leaf" @submit.prevent="submit"> <form class="leaf" @submit.prevent="submit">
<div class="leaf__head"> <div class="leaf__head">
<!-- Paper at last: the mark as it was painted, on paper of its own --> <!-- Paper at last: the logo's own colours, as it was drawn -->
<img class="leaf__logo" :src="sLogo" alt="Daydream" /> <img class="leaf__logo" :src="sLogo" alt="MyThoughts" />
<h2 class="leaf__title"> <h2 class="leaf__title">
{{ bNewBook ? lang.get('account.sign_up') : lang.get('account.sign_in') }} {{ bNewBook ? lang.get('account.sign_up') : lang.get('account.sign_in') }}
</h2> </h2>
+20 -98
View File
@@ -8,8 +8,7 @@ import { getDayKey } from '@scripts/time';
//the book paginates one page per view instead. //the book paginates one page per view instead.
const SINGLE_PAGE_WIDTH = 860; const SINGLE_PAGE_WIDTH = 860;
//Must outlast $turn in _var.scss, or the leaf is torn away mid-flight const TURN_MS = 700;
const TURN_MS = 500;
/** /**
* The open book. * The open book.
@@ -35,7 +34,7 @@ export default {
bookPage bookPage
}, },
emits: ['reading'], emits: ['reading'],
inject: ['journal', 'lang', 'user'], inject: ['journal', 'lang'],
data() { data() {
return { return {
iSpread: 0, iSpread: 0,
@@ -56,17 +55,6 @@ export default {
asCaret: null, asCaret: null,
asSelection: [], asSelection: [],
//Whether the book should keep the caret in view. True while writing,
//false once the reader has deliberately turned or jumped somewhere
//else - otherwise the next re-measure drags them back to the page
//being written on.
bFollowCaret: true,
//An entry jumped to from the rail or the calendar. Held so that
//"where you are" is the entry you asked for, not whichever one
//happens to open the page it starts on.
iFocusId: 0,
//The leaf in flight: {id, dir, front, back, from} while a page turns //The leaf in flight: {id, dir, front, back, from} while a page turns
asTurn: null, asTurn: null,
iTurnId: 0, iTurnId: 0,
@@ -156,18 +144,9 @@ export default {
offset: iOffset offset: iOffset
}; };
}, },
/** //What the rail highlights and the calendar opens on: the entry the
* What the rail highlights and the calendar opens on. //visible spread starts with.
*
* Normally the entry the visible spread starts with. But an entry can
* begin partway down a page that opens with the tail of an earlier one -
* and when you have just clicked that entry's bookmark, the one you
* asked for is where you are, not the one above it. So a jumped-to entry
* holds the position for as long as it is on the spread.
*/
currentEntryId() { currentEntryId() {
if((this.iFocusId > 0) && this.spreadShows(this.iFocusId)) return this.iFocusId;
for(const oPage of this.visiblePages) { for(const oPage of this.visiblePages) {
if(oPage.lines.length > 0) return oPage.lines[0].id; if(oPage.lines.length > 0) return oPage.lines[0].id;
} }
@@ -217,13 +196,6 @@ export default {
iPagesPerView() { iPagesPerView() {
this.$nextTick(() => this.measure()); this.$nextTick(() => this.measure());
}, },
//A different hand breaks the lines in different places, so the whole
//book has to be re-measured and re-flowed - nothing about the old
//layout survives a change of font.
'user.hand'() {
this.oPaginator.reset();
this.$nextTick(() => this.measure());
},
//The rail highlights, and the calendar opens on, whatever is being read //The rail highlights, and the calendar opens on, whatever is being read
currentEntryId: { currentEntryId: {
immediate: true, immediate: true,
@@ -306,10 +278,7 @@ export default {
if(this.oPaginator.setMetrics(oMeasure, iLineHeight, iLines)) this.relayout(); if(this.oPaginator.setMetrics(oMeasure, iLineHeight, iLines)) this.relayout();
this.bReady = true; this.bReady = true;
this.$nextTick(() => this.paintOverlay());
//Re-measuring can move the caret onto a different spread, so the
//caret is re-derived here rather than simply repainted where it was.
this.$nextTick(() => this.syncCaret());
}, },
//Geometry of the column the input and the measurer overlay //Geometry of the column the input and the measurer overlay
@@ -475,8 +444,6 @@ export default {
}, },
onInput(oEvent) { onInput(oEvent) {
//Typing goes into the open entry, so the book has to be showing it
this.bFollowCaret = true;
this.journal.write(oEvent.target.value); this.journal.write(oEvent.target.value);
this.relayout(); this.relayout();
this.syncCaret(); this.syncCaret();
@@ -494,20 +461,12 @@ export default {
const oInput = this.$refs.input; const oInput = this.$refs.input;
if(!oInput) return; if(!oInput) return;
//Read the focus rather than trusting the event: focusing an element
//that already has the focus fires nothing, so on load - where the
//book focuses itself twice - the flag would never be set and the
//caret would stay hidden until the first keystroke.
this.bFocused = (document.activeElement === oInput);
this.iSelectionStart = oInput.selectionStart; this.iSelectionStart = oInput.selectionStart;
this.iSelectionEnd = oInput.selectionEnd; this.iSelectionEnd = oInput.selectionEnd;
this.iCaretOffset = (oInput.selectionDirection === 'backward') ? oInput.selectionStart : oInput.selectionEnd; this.iCaretOffset = (oInput.selectionDirection === 'backward') ? oInput.selectionStart : oInput.selectionEnd;
//Following happens when the writing moves, not every time the caret
//is re-read: a re-measure must not undo a page turn.
const oPos = this.caretPosition; const oPos = this.caretPosition;
if(this.bFollowCaret && this.bFocused && oPos && (oPos.spread !== this.iSpread)) this.setSpread(oPos.spread); if(oPos && (oPos.spread !== this.iSpread)) this.setSpread(oPos.spread);
this.$nextTick(() => this.paintOverlay()); this.$nextTick(() => this.paintOverlay());
}, },
@@ -516,9 +475,6 @@ export default {
const oInput = this.$refs.input; const oInput = this.$refs.input;
if(!oInput) return; if(!oInput) return;
//Putting the caret somewhere is asking to write there
this.bFollowCaret = true;
oInput.focus({preventScroll: true}); oInput.focus({preventScroll: true});
if(bExtend) oInput.setSelectionRange(Math.min(oInput.selectionStart, iOffset), Math.max(oInput.selectionEnd, iOffset)); if(bExtend) oInput.setSelectionRange(Math.min(oInput.selectionStart, iOffset), Math.max(oInput.selectionEnd, iOffset));
@@ -629,60 +585,38 @@ export default {
const sDir = sDirection || ((iNext > iFrom) ? 'forward' : 'back'); const sDir = sDirection || ((iNext > iFrom) ? 'forward' : 'back');
const bForward = (sDir === 'forward'); const bForward = (sDir === 'forward');
//Moving to the facing spread is a page turn and looks like one. A
//jump across the book - to a bookmark, a date, or the writing on
//load - is not, so it does not pretend to be. (One page per view has
//no gutter to hinge on; the template gives it a slide instead.)
const bTurn = (Math.abs(iNext - iFrom) === 1);
//Keys the leaf element. Turning again before the last turn finished //Keys the leaf element. Turning again before the last turn finished
//has to build a new leaf, or Vue patches the old one in place and //has to build a new leaf, or Vue patches the old one in place and
//the CSS animation carries on from wherever it had got to. //the CSS animation carries on from wherever it had got to.
this.iTurnId++; this.iTurnId++;
this.asTurn = bTurn ? { if(this.iPagesPerView > 1) {
id: this.iTurnId, this.asTurn = {
dir: sDir, id: this.iTurnId,
from: iFrom, dir: sDir,
//Front: the face you were reading. Back: what it reveals. from: iFrom,
front: bForward ? ((iFrom * 2) + 1) : (iFrom * 2), //Front: the face you were reading. Back: what it reveals.
back: bForward ? (iNext * 2) : ((iNext * 2) + 1) front: bForward ? ((iFrom * 2) + 1) : (iFrom * 2),
} : null; back: bForward ? (iNext * 2) : ((iNext * 2) + 1)
};
}
else this.asTurn = {id: this.iTurnId, dir: sDir, from: iFrom, front: -1, back: -1};
this.iSpread = iNext; this.iSpread = iNext;
clearTimeout(this.oTurnTimer); clearTimeout(this.oTurnTimer);
this.oTurnTimer = setTimeout(() => {
if(bTurn) { this.asTurn = null;
this.oTurnTimer = setTimeout(() => { }, TURN_MS);
this.asTurn = null;
//The overlay must be redrawn once the leaf is gone: while it
//was flying, one side of the spread was showing the page it
//lifted from, so the caret had no page to be measured
//against and was left undrawn.
this.$nextTick(() => this.paintOverlay());
}, TURN_MS);
}
this.$nextTick(() => this.paintOverlay()); this.$nextTick(() => this.paintOverlay());
}, },
spreadShows(iEntryId) {
return this.visiblePages.some((oPage) => oPage.lines.some((oLine) => oLine.id === iEntryId));
},
sideFor(iPage) { sideFor(iPage) {
return ((iPage % 2) === 0) ? 'left' : 'right'; return ((iPage % 2) === 0) ? 'left' : 'right';
}, },
async turn(iDelta) { async turn(iDelta) {
//Turning the page by hand means you are reading the book again, not
//sitting on the entry you jumped to - and not being pulled back to
//the writing by the next re-measure.
this.iFocusId = 0;
this.bFollowCaret = false;
//Turning past either edge of the loaded window fetches more book //Turning past either edge of the loaded window fetches more book
//before it turns, so the flow never dead-ends on a chunk boundary. //before it turns, so the flow never dead-ends on a chunk boundary.
if((iDelta < 0) && (this.iSpread === 0)) { if((iDelta < 0) && (this.iSpread === 0)) {
@@ -742,8 +676,6 @@ export default {
const oPos = this.asLayout.index.get(iEntryId); const oPos = this.asLayout.index.get(iEntryId);
if(!oPos) return; if(!oPos) return;
this.iFocusId = iEntryId;
this.bFollowCaret = false;
this.setSpread(Math.floor(oPos.firstPage / this.iPagesPerView)); this.setSpread(Math.floor(oPos.firstPage / this.iPagesPerView));
}, },
@@ -792,16 +724,6 @@ export default {
@pick="onPick" @pick="onPick"
/> />
<!-- The shadow the leaf throws onto the page it is coming down
onto, so that page reads as lying underneath rather than as
a second column of text beside it. -->
<span
v-if="asTurn && (iPagesPerView > 1)"
:key="'cast' + asTurn.id"
class="book__cast"
:class="'book__cast--' + asTurn.dir"
></span>
<!-- The leaf in flight. Two real pages back to back, hinged on <!-- The leaf in flight. Two real pages back to back, hinged on
the gutter - the front is the page being lifted away, the the gutter - the front is the page being lifted away, the
back is the one it carries into view. --> back is the one it carries into view. -->
+6 -59
View File
@@ -1,33 +1,6 @@
<script> <script>
import appIcon from '@components/AppIcon'; import appIcon from '@components/AppIcon';
import { formatShortDate, formatTime, getDayKey } from '@scripts/time'; import { formatShortDate, formatTime } from '@scripts/time';
/* The dyes, matching $tabs in _color.scss - the three ribbons in the logo, and
* the same three dyed deeper.
*
* Chosen by the day an entry was written rather than by its position, so the
* colour means something: entries from one day share a tab colour, and a run of
* the same colour down the rail is a day you wrote a lot. Position-based colour
* would look the same and say nothing. */
const TAB_DYES = ['#eab469', '#a2b097', '#c7a4b8', '#bc781a', '#697a5c', '#85526f'];
/**
* A stable dye for a day.
*
* The day key is turned into a number and taken modulo the palette, so the same
* date always gets the same colour - on every reload, in every session, for
* however far back the book goes. Consecutive days land on different dyes,
* which is the whole point of dyeing them.
*/
function dyeForDay(sDay) {
//YYYY-MM-DD -> a day number. Not calendar-exact, and it does not need to
//be: it only has to be stable and to step by one from one day to the next.
const asParts = sDay.split('-').map(Number);
if(asParts.length !== 3 || asParts.some(isNaN)) return TAB_DYES[0];
const iDays = (asParts[0] * 372) + (asParts[1] * 31) + asParts[2];
return TAB_DYES[((iDays % TAB_DYES.length) + TAB_DYES.length) % TAB_DYES.length];
}
//Bookmarks shown either side of the entry being read. A whole book's worth of //Bookmarks shown either side of the entry being read. A whole book's worth of
//tabs is a scrollbar, not a rail - a handful around where you are is something //tabs is a scrollbar, not a rail - a handful around where you are is something
@@ -63,8 +36,7 @@ export default {
date: formatShortDate(oBookmark.time, oBookmark.timezone, this.lang.locale), date: formatShortDate(oBookmark.time, oBookmark.timezone, this.lang.locale),
time: formatTime(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 || this.lang.get('book.empty_entry'),
open: (oBookmark.status === 'open'), open: (oBookmark.status === 'open')
dye: dyeForDay(getDayKey(oBookmark.time, oBookmark.timezone))
})); }));
}, },
/** /**
@@ -101,19 +73,6 @@ export default {
this.$nextTick(() => this.scrollToCurrent()); this.$nextTick(() => this.scrollToCurrent());
}, },
methods: { methods: {
/**
* Walk the window one reach further back, by reading from its topmost
* tab. At the top of what is loaded there is nothing to walk to, so this
* fetches older entries instead.
*/
stepBack() {
if(this.hasBefore) this.$emit('pick', this.shown[0].id);
else this.$emit('load-older');
},
stepForward() {
if(this.hasAfter) this.$emit('pick', this.shown.at(-1).id);
},
scrollToCurrent() { scrollToCurrent() {
const oList = this.$refs.list; const oList = this.$refs.list;
if(!oList) return; if(!oList) return;
@@ -130,15 +89,14 @@ export default {
<nav class="rail" :aria-label="lang.get('book.bookmarks')"> <nav class="rail" :aria-label="lang.get('book.bookmarks')">
<span class="rail__title">{{ lang.get('book.bookmarks') }}</span> <span class="rail__title">{{ lang.get('book.bookmarks') }}</span>
<!-- The book carries on above the window - or, at the top of what is <!-- There is more book above the window, or older entries still to load -->
loaded, there is more of it still to fetch. -->
<button <button
v-if="hasBefore || hasOlder" v-if="hasBefore || hasOlder"
type="button" type="button"
class="rail__more rail__more--before" class="rail__more"
:disabled="loading" :disabled="loading"
:title="lang.get('book.earlier')" :title="lang.get('action.prev_page')"
@click="stepBack" @click="$emit('load-older')"
> >
<appIcon icon="chevronLeft" size="0.8em" /> <appIcon icon="chevronLeft" size="0.8em" />
</button> </button>
@@ -153,7 +111,6 @@ export default {
'rail__tab--current': (oTab.id === currentId), 'rail__tab--current': (oTab.id === currentId),
'rail__tab--open': (oTab.id === openId) 'rail__tab--open': (oTab.id === openId)
}" }"
:style="{'--tab': oTab.dye, '--tab-wash': oTab.dye + '40'}"
@click="$emit('pick', oTab.id)" @click="$emit('pick', oTab.id)"
> >
<span class="rail__date"> <span class="rail__date">
@@ -164,16 +121,6 @@ export default {
</button> </button>
</div> </div>
<button
v-if="hasAfter"
type="button"
class="rail__more rail__more--after"
:title="lang.get('book.later')"
@click="stepForward"
>
<appIcon icon="chevronRight" size="0.8em" />
</button>
<span v-if="tabs.length === 0" class="rail__empty">{{ lang.get('book.first_page') }}</span> <span v-if="tabs.length === 0" class="rail__empty">{{ lang.get('book.first_page') }}</span>
</nav> </nav>
</template> </template>
-44
View File
@@ -1,6 +1,5 @@
<script> <script>
import appIcon from '@components/AppIcon'; import appIcon from '@components/AppIcon';
import { applyHand, loadHand } from '@scripts/hands';
/** /**
* Account settings, saved one field at a time. * Account settings, saved one field at a time.
@@ -36,9 +35,6 @@ export default {
return [this.user.timezone].filter((sZone) => sZone !== ''); return [this.user.timezone].filter((sZone) => sZone !== '');
} }
}, },
hands() {
return this.consts.hands || [];
},
languageNames() { languageNames() {
const oNames = new Intl.DisplayNames([this.lang.locale], {type: 'language'}); const oNames = new Intl.DisplayNames([this.lang.locale], {type: 'language'});
return this.languages.map((sCode) => ({ return this.languages.map((sCode) => ({
@@ -48,23 +44,6 @@ export default {
} }
}, },
methods: { methods: {
/**
* Show the hand on the page as soon as it is picked, before the save
* comes back - choosing a hand is a visual decision, and waiting on a
* round trip to see it makes the picker feel broken.
*/
async pickHand(sHandId) {
if(sHandId === this.user.hand) return;
await loadHand(this.hands, sHandId);
applyHand(this.hands, sHandId);
await this.set('hand', sHandId);
//The save is what settles it; if it failed, go back to the one the
//account actually has rather than leaving a lie on screen.
if(this.sError !== '') applyHand(this.hands, this.user.hand);
},
async set(sField, sValue) { async set(sField, sValue) {
this.bBusy = true; this.bBusy = true;
this.sError = ''; this.sError = '';
@@ -147,29 +126,6 @@ export default {
</select> </select>
</div> </div>
<div class="leaf__row">
<span class="paper-label">{{ lang.get('account.hand') }}</span>
<div class="hand-picker" role="radiogroup" :aria-label="lang.get('account.hand')">
<button
v-for="oHand in hands"
:key="oHand.id"
type="button"
class="hand-picker__option"
:class="(oHand.id === user.hand) ? 'hand-picker__option--on' : null"
role="radio"
:aria-checked="oHand.id === user.hand"
:disabled="bBusy"
:style="{fontFamily: oHand.family + ', cursive'}"
@click="pickHand(oHand.id)"
>
<!-- Written in itself: the only thing worth knowing about
a hand is what your words look like in it. -->
<span class="hand-picker__sample">{{ lang.get('account.hand_sample') }}</span>
<span class="hand-picker__name">{{ oHand.name }}</span>
</button>
</div>
</div>
<div class="leaf__row"> <div class="leaf__row">
<label class="paper-label" for="set-tz">{{ lang.get('account.timezone') }}</label> <label class="paper-label" for="set-tz">{{ lang.get('account.timezone') }}</label>
<select <select
Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

+23
View File
@@ -0,0 +1,23 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 180 180" role="img" aria-label="MyThoughts">
<title>MyThoughts</title>
<defs>
<linearGradient id="desk" x1="0" y1="0" x2="0.4" y2="1">
<stop offset="0" stop-color="#44321e"/>
<stop offset="0.55" stop-color="#2c2114"/>
<stop offset="1" stop-color="#1a140d"/>
</linearGradient>
</defs>
<!-- Touch icons are masked by the OS, so the artwork keeps clear of the edges -->
<rect width="180" height="180" fill="url(#desk)"/>
<g transform="translate(90 92) scale(2.55) translate(-32 -33)">
<path d="M32 19.5C27.7 15.9 22.2 14.2 14.5 14.2v32.4c7.7 0 13.2 1.7 17.5 5.2 4.3-3.5 9.8-5.2 17.5-5.2V14.2c-7.7 0-13.2 1.7-17.5 5.3z"
fill="#f8f2e4" stroke="#cdbb99" stroke-width="1.6" stroke-linejoin="round"/>
<path d="M32 19.5v32.3" stroke="#cdbb99" stroke-width="2" stroke-linecap="round"/>
<g stroke="#6d6358" stroke-width="1.7" stroke-linecap="round" opacity="0.75">
<path d="M20.5 26.5h7.5"/>
<path d="M20.5 32h7.5"/>
<path d="M20.5 37.5h5"/>
</g>
<path d="M36.5 14.6h6v16l-3-2.4-3 2.4z" fill="#795731"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

+17
View File
@@ -0,0 +1,17 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-label="MyThoughts">
<title>MyThoughts</title>
<rect width="64" height="64" rx="12" fill="#2c2114"/>
<!-- The two pages, splayed from the gutter -->
<path d="M32 19.5C27.7 15.9 22.2 14.2 14.5 14.2v32.4c7.7 0 13.2 1.7 17.5 5.2 4.3-3.5 9.8-5.2 17.5-5.2V14.2c-7.7 0-13.2 1.7-17.5 5.3z"
fill="#f8f2e4" stroke="#cdbb99" stroke-width="1.6" stroke-linejoin="round"/>
<!-- The gutter -->
<path d="M32 19.5v32.3" stroke="#cdbb99" stroke-width="2" stroke-linecap="round"/>
<!-- Writing on the left page, trailing off the way a page in progress does -->
<g stroke="#6d6358" stroke-width="1.7" stroke-linecap="round" opacity="0.75">
<path d="M20.5 26.5h7.5"/>
<path d="M20.5 32h7.5"/>
<path d="M20.5 37.5h5"/>
</g>
<!-- The ribbon, marking today's page -->
<path d="M36.5 14.6h6v16l-3-2.4-3 2.4z" fill="#795731"/>
</svg>

After

Width:  |  Height:  |  Size: 903 B

+11 -11
View File
@@ -1,23 +1,23 @@
{ {
"name": "Daydream", "name": "MyThoughts",
"short_name": "Daydream", "short_name": "MyThoughts",
"description": "A quiet place to write. Where the mind wanders, on paper.", "description": "A quiet place to write. Your thoughts, on paper.",
"start_url": "../../../", "start_url": "../../../",
"scope": "../../../", "scope": "../../../",
"display": "standalone", "display": "standalone",
"orientation": "any", "orientation": "any",
"background_color": "#fef9ed", "background_color": "#2c2114",
"theme_color": "#fef9ed", "theme_color": "#2c2114",
"icons": [ "icons": [
{ {
"src": "favicon.png", "src": "favicon.svg",
"sizes": "128x128", "sizes": "any",
"type": "image/png" "type": "image/svg+xml"
}, },
{ {
"src": "apple-touch-icon.png", "src": "apple-touch-icon.svg",
"sizes": "180x180", "sizes": "any",
"type": "image/png", "type": "image/svg+xml",
"purpose": "maskable" "purpose": "maskable"
} }
] ]
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

Regular → Executable
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 298 KiB

After

Width:  |  Height:  |  Size: 55 KiB

-54
View File
@@ -1,54 +0,0 @@
/* The webfonts for every hand the book can be written in.
*
* Side-effect imports only: these declare @font-face rules and nothing more.
* A declared face costs nothing until something is rendered in it, so shipping
* all six is a few KB of CSS - the woff2 files are fetched only for the hand
* actually in use, plus whatever the settings picker previews when it is opened.
*
* The list itself lives in Daydream::HANDS on the server, which is also what
* validates a change - adding one there means adding its import here.
*/
import '@fontsource-variable/caveat/index.css';
import '@fontsource/kalam/400.css';
import '@fontsource/patrick-hand/400.css';
import '@fontsource/shadows-into-light/400.css';
import '@fontsource/architects-daughter/400.css';
import '@fontsource/indie-flower/400.css';
//Fallbacks appended to whichever hand is chosen, so the book still reads as
//handwriting if a webfont fails
const FALLBACK = '"Segoe Script", cursive';
/**
* Put a hand on the page.
*
* Both values are set together and read by the book's type metrics: the family
* and the size that makes that family sit on the ruled lines. Setting one
* without the other gives writing that floats above the rule or crashes
* through it.
*/
export function applyHand(asHands, sHandId) {
const oHand = asHands.find((oItem) => oItem.id === sHandId) || asHands[0];
if(!oHand) return null;
const oRoot = document.documentElement;
oRoot.style.setProperty('--book-hand', oHand.family + ', ' + FALLBACK);
oRoot.style.setProperty('--book-hand-scale', String(oHand.scale));
return oHand;
}
/** Wait for a hand's files to arrive, so the book is not measured in the fallback */
export async function loadHand(asHands, sHandId, sSizeSample = '16px') {
const oHand = asHands.find((oItem) => oItem.id === sHandId);
if(!oHand || !document.fonts?.load) return;
try {
await document.fonts.load(sSizeSample + ' ' + oHand.family.split(',')[0].trim());
}
catch{
//A hand that will not load is not worth failing the page over - the
//fallback stack still renders, and the next measure picks up the real
//font whenever it does arrive.
}
}
-13
View File
@@ -44,19 +44,6 @@ export default class Paginator {
return true; return true;
} }
/**
* Throw away every measurement taken so far.
*
* For a change of font, where setMetrics() cannot help: the column width,
* the line height and the lines per page can all come back identical, so
* the signature says nothing has changed - while every line in the book now
* breaks somewhere else.
*/
reset() {
this.asCache.clear();
this.sSignature = '';
}
/* Layout */ /* Layout */
/** /**
+23 -41
View File
@@ -31,44 +31,30 @@
font-weight: normal; font-weight: normal;
} }
/* The mark is the painting itself, carried as a layer over its own sheet: what /* The logo is dark roast ink on a transparent bubble - drawn for paper, not for
* each pixel took away from the paper is its opacity, so the book arrives with * the desk. Rather than sit it on a light patch and break the surface, it is
* its colours and its shadow arrives as a shadow. It needs no plate behind it * re-inked in cream: brightness(0) flattens it to a silhouette, invert lifts it
* and no shadow added to it - it darkens the desk here and the paper of the * to white, and the sepia/hue pass warms that back to the colour of the pages.
* sign-in card by exactly as much as it darkened the sheet it was painted on. */ * Its full colour is kept for the sign-in leaf, which is paper. */
.app__logo { .app__logo {
//Below about 80px wide the ribbons stop reading as ribbons, so the mark is height: 3.3rem;
//given the room it needs rather than shrunk to fit
height: 3.5rem;
width: auto; width: auto;
display: block; display: block;
flex: 0 0 auto; //brightness(0) flattens the artwork to a silhouette, invert lifts it to
} //white, and the short sepia pass warms that to the colour of the pages.
//Kept deliberately simple - a longer chain only muddies it.
.app__name { filter:
display: flex; brightness(0)
flex-direction: column; invert(1)
justify-content: center; sepia(0.22)
min-width: 0; saturate(1.5)
line-height: 1.05; drop-shadow(0 1px 1px rgba(0, 0, 0, 0.4));
} opacity: 0.95;
/* The name is set in the book's own hand rather than drawn as paths: it is
* already loaded, it stays crisp at any size, and it changes with the hand the
* reader chose - so the masthead is written in the same writing as the pages. */
.app__wordmark {
font-family: var.$font-hand;
font-size: 2rem;
letter-spacing: 0.01em;
color: color.$ink;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
} }
.app__tagline { .app__tagline {
font-size: 0.8rem; font-size: 0.8rem;
color: color.$ink-soft; color: rgba(255, 238, 210, 0.58);
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
@@ -91,9 +77,7 @@
justify-content: center; justify-content: center;
gap: 0; gap: 0;
min-height: 0; min-height: 0;
//Room for the desk. The book is still the app, but a book with no desk padding: 0 clamp(0.5rem, 2vw, 2rem) clamp(0.75rem, 2.5vh, 2rem);
//around it is a page, and the mark is a book lying on something.
padding: clamp(0.25rem, 1.2vh, 0.9rem) clamp(0.5rem, 2vw, 2rem) clamp(0.9rem, 3vh, 2.2rem);
} }
.app__book { .app__book {
@@ -144,23 +128,21 @@
padding: 0.5rem 0.9rem; padding: 0.5rem 0.9rem;
border-radius: 999px; border-radius: 999px;
font-size: 0.85rem; font-size: 0.85rem;
//The one thing that is allowed to be dark on the desk: the cover of the color: #fff4e2;
//book, borrowed for a strip that has to be read before it goes away background-color: rgba(29, 21, 15, 0.92);
color: color.$paper;
background-color: color.$cover-deep;
box-shadow: var.$shadow-panel; box-shadow: var.$shadow-panel;
} }
.app__notice--bad { .app__notice--bad {
background-color: color.$bad; background-color: color.$ribbon-deep;
} }
.app__notice-close { .app__notice-close {
color: rgba(254, 244, 225, 0.6); color: rgba(255, 244, 226, 0.6);
display: inline-flex; display: inline-flex;
&:hover { &:hover {
color: color.$paper; color: #fff4e2;
} }
} }
+13 -126
View File
@@ -38,14 +38,7 @@
background-color: color.$paper-edge; background-color: color.$paper-edge;
box-shadow: var.$shadow-page; box-shadow: var.$shadow-page;
//The page block: a few stacked cut edges peeking out below and to the sides, //The page block: a few stacked cut edges peeking out below and to the sides
//and the board they are bound into.
//
//The cover is what makes the book an object here. On the ivory desk cream
//paper against cream is barely an edge; in the mark it is the dark brown
//board around the paper that you actually read as a book, so the block
//carries one - a ring of cover just past the cut edges, and the shadow it
//throws on the desk.
&::before { &::before {
content: ""; content: "";
position: absolute; position: absolute;
@@ -60,10 +53,7 @@
color.$paper-edge 62%, color.$paper-edge 62%,
color.$paper-deep 100% color.$paper-deep 100%
); );
box-shadow: box-shadow: 0 0.9rem 1.6rem color.$shadow-deep;
0 0 0 0.5rem color.$cover,
0 0 0 calc(0.5rem + 1px) color.$cover-deep,
0 1rem 1.8rem color.$shadow-deep;
} }
} }
@@ -223,7 +213,7 @@
* three of them take their typography from exactly one place: here. */ * three of them take their typography from exactly one place: here. */
@mixin page-text-metrics { @mixin page-text-metrics {
font-family: var.$font-hand; font-family: var.$font-hand;
font-size: calc(var(--book-line) * #{var.$hand-scale}); font-size: calc(var(--book-line) * 0.82);
line-height: var(--book-line); line-height: var(--book-line);
letter-spacing: 0.005em; letter-spacing: 0.005em;
word-spacing: 0.02em; word-spacing: 0.02em;
@@ -420,13 +410,13 @@
.book__leaf--forward { .book__leaf--forward {
left: 50%; left: 50%;
transform-origin: left center; transform-origin: left center;
animation: leaf-forward var.$turn cubic-bezier(0.4, 0.06, 0.3, 0.96) forwards; animation: leaf-forward var.$trans-slow cubic-bezier(0.4, 0.06, 0.3, 0.96) forwards;
} }
.book__leaf--back { .book__leaf--back {
left: 0; left: 0;
transform-origin: right center; transform-origin: right center;
animation: leaf-back var.$turn cubic-bezier(0.4, 0.06, 0.3, 0.96) forwards; animation: leaf-back var.$trans-slow cubic-bezier(0.4, 0.06, 0.3, 0.96) forwards;
} }
//Right page sweeping left across the gutter //Right page sweeping left across the gutter
@@ -457,7 +447,7 @@
overflow: hidden; overflow: hidden;
backface-visibility: hidden; backface-visibility: hidden;
background-color: color.$paper; background-color: color.$paper;
box-shadow: 0 0 2.4rem rgba(20, 14, 9, 0.5), 0 0 0 1px rgba(120, 96, 62, 0.18); box-shadow: 0 0 2rem rgba(20, 14, 9, 0.35);
//The page fills the leaf rather than half a spread //The page fills the leaf rather than half a spread
.page { .page {
@@ -471,47 +461,6 @@
transform: rotateY(180deg); transform: rotateY(180deg);
} }
/* Which face you can see is switched explicitly at the upright crossing,
* rather than left to `backface-visibility` alone.
*
* That property is the right tool and is set above, but engines disagree about
* it once `preserve-3d` and a clipped ancestor are involved - and when it is
* ignored the front face shows straight through the back one, putting both
* pages' words on top of each other for the length of the turn. The switch
* point is where the leaf passes 90 degrees under the easing above (measured,
* not guessed - the rotation is not linear in time). */
.book__leaf-face--front {
animation: leaf-face-front var.$turn linear forwards;
}
.book__leaf-face--back {
animation: leaf-face-back var.$turn linear forwards;
}
@keyframes leaf-face-front {
0%,
38% {
visibility: visible;
}
39%,
100% {
visibility: hidden;
}
}
@keyframes leaf-face-back {
0%,
38% {
visibility: hidden;
}
39%,
100% {
visibility: visible;
}
}
/* Paper catches the light as it lifts and loses it as it lands. Two passes of /* Paper catches the light as it lifts and loses it as it lands. Two passes of
* the same shade, offset by half the turn, is what sells the leaf as solid. */ * the same shade, offset by half the turn, is what sells the leaf as solid. */
.book__leaf-shade { .book__leaf-shade {
@@ -522,12 +471,12 @@
.book__leaf-face--front .book__leaf-shade { .book__leaf-face--front .book__leaf-shade {
background-image: linear-gradient(to left, rgba(20, 14, 9, 0.42), rgba(20, 14, 9, 0.04) 45%, transparent 75%); background-image: linear-gradient(to left, rgba(20, 14, 9, 0.42), rgba(20, 14, 9, 0.04) 45%, transparent 75%);
animation: leaf-shade-out var.$turn ease-in forwards; animation: leaf-shade-out var.$trans-slow ease-in forwards;
} }
.book__leaf-face--back .book__leaf-shade { .book__leaf-face--back .book__leaf-shade {
background-image: linear-gradient(to right, rgba(20, 14, 9, 0.46), rgba(20, 14, 9, 0.06) 45%, transparent 75%); background-image: linear-gradient(to right, rgba(20, 14, 9, 0.46), rgba(20, 14, 9, 0.06) 45%, transparent 75%);
animation: leaf-shade-in var.$turn ease-out forwards; animation: leaf-shade-in var.$trans-slow ease-out forwards;
} }
@keyframes leaf-shade-out { @keyframes leaf-shade-out {
@@ -550,71 +499,9 @@
} }
} }
/* The shadow a leaf casts on the page it is coming down onto.
*
* This is what stops the page underneath reading as a second page of text
* beside the leaf: real paper coming over a page darkens it, and the eye uses
* that to tell which sheet is on top. Sits above the pages and below the leaf.
*/
.book__cast {
position: absolute;
top: 0;
bottom: 0;
width: 50%;
z-index: 6;
pointer-events: none;
opacity: 0;
}
.book__cast--forward {
left: 0;
background-image: linear-gradient(to left, rgba(24, 17, 10, 0.62), rgba(24, 17, 10, 0.34) 45%, rgba(24, 17, 10, 0.16) 100%);
animation: cast-forward var.$turn cubic-bezier(0.4, 0.06, 0.3, 0.96) forwards;
}
.book__cast--back {
left: 50%;
background-image: linear-gradient(to right, rgba(24, 17, 10, 0.62), rgba(24, 17, 10, 0.34) 45%, rgba(24, 17, 10, 0.16) 100%);
animation: cast-back var.$turn cubic-bezier(0.4, 0.06, 0.3, 0.96) forwards;
}
/* Deepens from the moment the leaf starts to lift - a raised sheet shades the
* spread under it - holds through the crossing, where the page being left and
* the page being uncovered are both in view, and lifts once the leaf has landed
* and is itself the page being read. */
@keyframes cast-forward {
0% {
opacity: 0;
}
22%,
66% {
opacity: 1;
}
100% {
opacity: 0;
}
}
@keyframes cast-back {
0% {
opacity: 0;
}
22%,
66% {
opacity: 1;
}
100% {
opacity: 0;
}
}
/* The gutter darkens under a leaf that is standing up over it */ /* The gutter darkens under a leaf that is standing up over it */
.book__turning .book__spine { .book__turning .book__spine {
animation: gutter-deepen var.$turn ease-in-out; animation: gutter-deepen var.$trans-slow ease-in-out;
} }
@keyframes gutter-deepen { @keyframes gutter-deepen {
@@ -631,11 +518,11 @@
/* One page per view has no gutter to hinge on, so the page is dealt off the /* One page per view has no gutter to hinge on, so the page is dealt off the
* pile in the direction of travel instead of pretending to be bound. */ * pile in the direction of travel instead of pretending to be bound. */
.book__spread--single.book__turning--forward .page { .book__spread--single.book__turning--forward .page {
animation: page-in-from-right var.$turn ease-out; animation: page-in-from-right var.$trans-mid ease-out;
} }
.book__spread--single.book__turning--back .page { .book__spread--single.book__turning--back .page {
animation: page-in-from-left var.$turn ease-out; animation: page-in-from-left var.$trans-mid ease-out;
} }
@keyframes page-in-from-right { @keyframes page-in-from-right {
@@ -700,8 +587,8 @@
.book__ribbon { .book__ribbon {
position: absolute; position: absolute;
top: 0; top: 0;
left: calc(50% - 0.425rem); left: calc(50% - 0.55rem);
width: 0.85rem; width: 1.1rem;
z-index: 4; z-index: 4;
pointer-events: none; pointer-events: none;
background-image: linear-gradient(to right, color.$ribbon-deep, color.$ribbon 45%, color.$ribbon-deep); background-image: linear-gradient(to right, color.$ribbon-deep, color.$ribbon 45%, color.$ribbon-deep);
+3 -3
View File
@@ -105,11 +105,11 @@
} }
.calendar__day--today { .calendar__day--today {
box-shadow: inset 0 0 0 1px color.$amber; box-shadow: inset 0 0 0 1px color.$caramel;
} }
.calendar__day--current { .calendar__day--current {
color: color.$paper; color: #fff6e6;
background-color: color.$ribbon; background-color: color.$ribbon;
&:hover { &:hover {
@@ -117,7 +117,7 @@
} }
&::after { &::after {
background-color: color.$paper; background-color: #fff6e6;
} }
} }
+41 -76
View File
@@ -1,87 +1,52 @@
/* Everything here is sampled off the logo. /* The palette is taken from the logo, not chosen alongside it.
* *
* The mark is an open book lying on warm ivory: a dark brown board, cream * src/images/logo.png is dark-roast lettering in a thought bubble with a cup of
* pages, a hand you cannot quite read, and three dyed ribbons hanging out of * coffee; sampling it gives one dominant near-black brown (#2c2114) and a run of
* the fore-edge. Every value below was read out of that drawing rather than * caramels from #795731 up to #b47e41. Those are the colours below. The book is
* guessed next to it - the sheet it is printed on is the desk, the pages are * the same idea in furniture: a coffee-dark desk, cream paper, and caramel for
* the paper, the board is the ink, and the three ribbons are the only colour * anything that has to catch the eye.
* the app is allowed to raise its voice with.
* *
* There is no pure grey, no pure black and no blue anywhere, because there is * There is no pure grey and no pure black anywhere - paper and lamplight do not
* none in the mark. Every neutral here is a brown that has been let go. * have any, and neither does the logo. */
*
* Where a dye had to carry text it is the same dye with the lightness taken
* down - hue and saturation untouched - until it clears 4.6:1. Nothing is
* invented; things are only ever put in shadow.
*/
//The desk: the sheet the mark is printed on, the light on it, and the corners //The desk the book lies on - straight from the logo's darkest inks
//falling away into the shadow the book throws $desk-deep: #1a140d;
$linen: #fef9ed; $desk: #2c2114;
$linen-light: #fffdf7; $desk-edge: #44321e;
$linen-deep: #f2e7d1;
//Paper. -shade is the tint towards the spine, -edge and -deep the cut edges of //Paper. -shade is the tint towards the spine, -edge the cut edge of the stack
//the stack, lit and in shadow $paper: #f8f2e4;
$paper: #fef4e1; $paper-shade: #f0e7d3;
$paper-shade: #fbf1dc; $paper-edge: #e0d3b8;
$paper-edge: #eed9b7; $paper-deep: #cdbb99;
$paper-deep: #d7b990;
//The board the pages are bound into. Cream paper on a cream desk is a rumour;
//in the mark it is the dark brown cover that makes the book an object
$cover-light: #774b31;
$cover: #5a361f;
$cover-deep: #361d0c;
//Ink, in the three weights the book uses: what you wrote, what the book says //Ink, in the three weights the book uses: what you wrote, what the book says
//about it, and what is barely there. The heaviest is the brown the wordmark is //about it, and what is barely there
//set in, the middle one is the lit face of the board, and the faintest is the $ink: #2e2822;
//hand the mark is written in $ink-soft: #6d6358;
$ink: #45220d; $ink-faint: #a29684;
$ink-soft: #774b31; $ink-ghost: rgba(46, 40, 34, 0.38);
$ink-faint: #a9957b;
$ink-ghost: rgba(69, 34, 13, 0.32);
//Ruling. Not ink: the rules are that same handwriting brown, faded until they //Ruling. The horizontal rules are cold on purpose - they are the one thing on
//are something the writing crosses rather than meets //the page that is not ink, and that contrast is what makes paper read as paper
$rule: rgba(169, 149, 123, 0.42); $rule: rgba(90, 120, 150, 0.22);
$rule-margin: rgba(223, 167, 91, 0.6); $rule-margin: rgba(168, 118, 62, 0.5);
//The gold ribbon: anything that has to catch the eye off the paper //Accents: the coffee and the steam swirl
$amber: #eab469; $caramel: #b47e41;
$amber-light: #f0bd74; $caramel-deep: #795731;
$amber-deep: #9a6316; $caramel-light: #d0a06a;
//The green ribbon //The ribbon marking the page being written on
$sage: #a2b097; $ribbon: $caramel-deep;
$sage-deep: #647458; $ribbon-deep: #5c4428;
//The lilac ribbon. It marks the page being written on - gold and green already //Feedback. Brick rather than red, so a warning still belongs to the palette
//have work, and this is the one the eye has nothing else to confuse with $ok: #5c6b3a;
$ribbon: #8e5777; $warn: #a8763e;
$ribbon-deep: #6f445c; $bad: #9c4a34;
/* Dyed index tabs. //Overlays
* $veil: rgba(20, 14, 9, 0.74);
* Six, cycled by the day an entry was written - so every entry from one day $shadow-soft: rgba(20, 14, 9, 0.18);
* shares a colour and the rail reads as a run of days rather than a list of $shadow-deep: rgba(20, 14, 9, 0.45);
* rows. They are the mark's three ribbons exactly as they were painted, then
* the same three in shadow, ordered so the hue changes at every step and no two
* days running look alike.
*/
$tabs: #eab469, #a2b097, #c7a4b8, #bc781a, #697a5c, #85526f;
//Feedback. Kept in the same register as everything else - a warning belongs to
//the palette, it does not shout over it. There is no red in the mark, so the
//one failure colour is the deepest brown in it pulled as far towards red as it
//can go and still belong
$ok: #647458;
$warn: #b37319;
$bad: #9b4631;
//Overlays. The book's own shadow on the sheet: warm, and never heavier than it
//is in the mark
$veil: rgba(54, 29, 12, 0.5);
$shadow-soft: rgba(90, 54, 31, 0.12);
$shadow-deep: rgba(54, 29, 12, 0.22);
+24 -31
View File
@@ -15,25 +15,19 @@ body {
font-family: var.$font-ui; font-family: var.$font-ui;
font-size: 16px; font-size: 16px;
color: color.$ink; color: color.$ink;
background-color: color.$linen; background-color: color.$desk-deep;
/* The desk the mark is printed on: warm ivory, lit where the book lies. //The desk: a warm pool of lamplight falling on wood, with the grain done as
* //a pair of very low-contrast repeating gradients rather than a bitmap.
* Three layers, front to back: the pool of light the book sits in, the same
* warmth spread wide enough to keep the middle of the screen from going flat,
* and the corners falling away into the deeper cream of the paper stack.
*
* All gradients, no bitmap - it costs nothing, resizes to any screen, and
* stays clean on a retina display where a photograph would not. */
background-image: background-image:
//The light the book lies in radial-gradient(ellipse 120% 90% at 50% -10%, rgba(255, 226, 178, 0.16), transparent 60%),
radial-gradient(ellipse 62% 52% at 50% 40%, #{color.$linen-light} 0%, rgba(255, 253, 247, 0) 72%), repeating-linear-gradient(
92deg,
//The warmth of that light, carried further out rgba(0, 0, 0, 0.05) 0 3px,
radial-gradient(ellipse 96% 78% at 50% 46%, rgba(255, 253, 247, 0.6) 0%, rgba(255, 253, 247, 0) 78%), rgba(255, 255, 255, 0.014) 3px 7px,
rgba(0, 0, 0, 0.035) 7px 11px
//The corners of the desk, where the light does not reach ),
radial-gradient(ellipse 118% 96% at 50% 44%, rgba(242, 231, 209, 0) 58%, #{color.$linen-deep} 100%); linear-gradient(160deg, color.$desk-edge 0%, color.$desk 45%, color.$desk-deep 100%);
background-attachment: fixed; background-attachment: fixed;
overflow: hidden; overflow: hidden;
@@ -61,35 +55,34 @@ textarea {
font: inherit; font: inherit;
color: inherit; color: inherit;
//Checkboxes and the like belong to the palette, not to the browser //Checkboxes and the like belong to the palette, not to the browser
accent-color: color.$amber-deep; accent-color: color.$caramel-deep;
} }
/* Buttons that sit on the desk rather than on the paper: a card of the same /* Buttons that sit on the desk rather than on the paper: brass-ish, restrained */
* paper as the book, laid on the ivory. Restrained - they are furniture */
.desk-button { .desk-button {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
gap: var.$text-spacing; gap: var.$text-spacing;
padding: 0.45rem 0.8rem; padding: 0.45rem 0.8rem;
border-radius: var.$block-radius; border-radius: var.$block-radius;
color: color.$ink-soft; color: rgba(255, 244, 224, 0.82);
background-color: rgba(254, 244, 225, 0.55); background-color: rgba(255, 240, 214, 0.07);
border: 1px solid color.$paper-edge; border: 1px solid rgba(255, 240, 214, 0.14);
transition: background-color var.$trans-quick, color var.$trans-quick, border-color var.$trans-quick; transition: background-color var.$trans-quick, color var.$trans-quick, border-color var.$trans-quick;
white-space: nowrap; white-space: nowrap;
&:hover, &:hover,
&:focus-visible { &:focus-visible {
color: color.$ink; color: #fff8ea;
background-color: color.$paper; background-color: rgba(255, 240, 214, 0.14);
border-color: color.$paper-deep; border-color: rgba(255, 240, 214, 0.28);
} }
&[aria-expanded="true"], &[aria-expanded="true"],
&.is-active { &.is-active {
color: color.$ink; color: #fff8ea;
background-color: color.$paper-shade; background-color: rgba(255, 240, 214, 0.18);
border-color: color.$paper-deep; border-color: rgba(255, 240, 214, 0.34);
} }
&:disabled { &:disabled {
@@ -116,7 +109,7 @@ textarea {
&:focus { &:focus {
outline: 0; outline: 0;
border-color: color.$ribbon; border-color: color.$ribbon;
box-shadow: 0 0 0 3px rgba(142, 87, 119, 0.18); box-shadow: 0 0 0 3px rgba(163, 55, 47, 0.14);
} }
} }
@@ -143,7 +136,7 @@ textarea {
} }
:focus-visible { :focus-visible {
outline: 2px solid color.$amber; outline: 2px solid color.$caramel;
outline-offset: 2px; outline-offset: 2px;
} }
+1 -14
View File
@@ -126,7 +126,7 @@
z-index: 90; z-index: 90;
width: min(17rem, 82vw); width: min(17rem, 82vw);
padding: 0.75rem 0.5rem 0.75rem 0.75rem; padding: 0.75rem 0.5rem 0.75rem 0.75rem;
background-color: rgba(54, 29, 12, 0.96); background-color: rgba(29, 21, 15, 0.96);
box-shadow: var.$shadow-panel; box-shadow: var.$shadow-panel;
transform: translateX(100%); transform: translateX(100%);
transition: transform var.$trans-mid ease-out; transition: transform var.$trans-mid ease-out;
@@ -158,21 +158,8 @@
max-width: 100%; max-width: 100%;
} }
/* The drawer is the inside of the back board, so the two bits of text that
* sit on it rather than on a tab have to come off the paper and onto the
* cover - everywhere else they are ink on ivory */
.rail__empty { .rail__empty {
writing-mode: horizontal-tb; writing-mode: horizontal-tb;
color: rgba(254, 244, 225, 0.55);
}
.rail__more {
color: rgba(254, 244, 225, 0.6);
&:hover {
color: color.$paper;
background-color: rgba(254, 244, 225, 0.12);
}
} }
.calendar { .calendar {
+9 -76
View File
@@ -1,4 +1,3 @@
@use "sass:color" as sass-color;
@use "@styles/color"; @use "@styles/color";
@use "@styles/var"; @use "@styles/var";
@@ -103,17 +102,14 @@
gap: var.$text-spacing; gap: var.$text-spacing;
padding: 0.55rem 1.1rem; padding: 0.55rem 1.1rem;
border-radius: var.$block-radius; border-radius: var.$block-radius;
//The gold ribbon with the wordmark brown on it, exactly as the two sit color: #fff6e6;
//together in the mark. Gold is the palette's one colour for things that background-color: color.$ribbon;
//have to be reached for; the lilac ribbon belongs to the book
color: color.$ink;
background-color: color.$amber;
box-shadow: var.$shadow-elem; box-shadow: var.$shadow-elem;
transition: background-color var.$trans-quick; transition: background-color var.$trans-quick;
&:hover:not(:disabled), &:hover:not(:disabled),
&:focus-visible { &:focus-visible {
background-color: sass-color.adjust(color.$amber, $lightness: 6%); background-color: color.$ribbon-deep;
} }
&:disabled { &:disabled {
@@ -136,12 +132,12 @@
} }
.text-button--bad { .text-button--bad {
color: color.$bad; color: color.$ribbon;
&:hover, &:hover,
&:focus-visible { &:focus-visible {
color: color.$bad; color: color.$ribbon-deep;
background-color: rgba(155, 70, 49, 0.09); background-color: rgba(163, 55, 47, 0.08);
} }
} }
@@ -152,7 +148,7 @@
align-items: center; align-items: center;
gap: 0.4rem; gap: 0.4rem;
font-size: 0.78rem; font-size: 0.78rem;
color: color.$ink-faint; color: rgba(255, 238, 210, 0.5);
padding: 0.35rem 0.5rem; padding: 0.35rem 0.5rem;
white-space: nowrap; white-space: nowrap;
transition: color var.$trans-quick; transition: color var.$trans-quick;
@@ -160,11 +156,11 @@
.saver--saving, .saver--saving,
.saver--pending { .saver--pending {
color: color.$ink-soft; color: rgba(255, 238, 210, 0.75);
} }
.saver--failed { .saver--failed {
color: color.$bad; color: #f0b6ae;
} }
.saver__dot { .saver__dot {
@@ -189,66 +185,3 @@
opacity: 1; opacity: 1;
} }
} }
/* Picking the hand the book is written in.
*
* Each option is set in the hand it offers, because the name of a typeface
* tells you nothing - what your own words look like in it is the whole
* decision. The sample line comes from the dictionary, so it reads as a
* sentence rather than as "Aa Bb Cc" in whatever language.
*/
.hand-picker {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr));
gap: 0.5rem;
}
.hand-picker__option {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 0.15rem;
padding: 0.6rem 0.75rem;
text-align: left;
cursor: pointer;
border: 1px solid color.$rule;
border-radius: var.$block-radius;
background-color: color.$paper;
color: color.$ink;
transition: border-color var.$trans-quick, background-color var.$trans-quick;
&:hover:not(:disabled) {
border-color: color.$ink-faint;
background-color: color.$paper-shade;
}
&:disabled {
opacity: 0.55;
cursor: default;
}
}
//The one in use: a ruled line under the sample, like the page itself
.hand-picker__option--on {
border-color: color.$amber;
background-color: color.$paper-shade;
box-shadow: inset 0 0 0 1px color.$amber;
}
.hand-picker__sample {
font-size: 1.45rem;
line-height: 1.35;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 100%;
}
//The name is the only part not set in the hand - it is a label, not a sample
.hand-picker__name {
font-family: var.$font-ui;
font-size: 0.72rem;
letter-spacing: 0.04em;
text-transform: uppercase;
color: color.$ink-faint;
}
+11 -26
View File
@@ -39,9 +39,6 @@
scrollbar-width: none; scrollbar-width: none;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
//The window is small and centred on where the reading is, so the current
//tab sits at eye level rather than wherever a full list would put it.
justify-content: center;
gap: 0.3rem; gap: 0.3rem;
padding: 0.15rem 0; padding: 0.15rem 0;
@@ -69,17 +66,9 @@
color: color.$ink-soft; color: color.$ink-soft;
background-color: color.$paper-shade; background-color: color.$paper-shade;
//The shadowed strip that reads as the part still inside the book //The shadowed strip that reads as the part still inside the book
background-image: background-image: linear-gradient(to right, rgba(90, 68, 40, 0.22), rgba(90, 68, 40, 0.04) var.$tab-tuck, transparent calc(var.$tab-tuck + 0.5rem));
linear-gradient(to right, rgba(90, 68, 40, 0.22), rgba(90, 68, 40, 0.04) var.$tab-tuck, transparent calc(var.$tab-tuck + 0.5rem)),
//The dye, washing in from the outer edge
linear-gradient(to right, transparent 30%, var(--tab-wash, transparent) 100%);
//`--tab` is set per tab from the day the entry was written, so a day's
//entries share a colour and the rail reads as a run of days rather than a
//list of rows. The card is still paper - only its edge is properly dyed.
border-right: 0.45rem solid var(--tab, #{color.$paper-deep});
border-radius: 0 var.$block-radius var.$block-radius 0; border-radius: 0 var.$block-radius var.$block-radius 0;
box-shadow: 0.12rem 0.15rem 0.5rem rgba(90, 54, 31, 0.22); box-shadow: 0.12rem 0.15rem 0.5rem rgba(20, 14, 9, 0.3);
transition: transform var.$trans-mid ease-out, background-color var.$trans-quick, color var.$trans-quick; transition: transform var.$trans-mid ease-out, background-color var.$trans-quick, color var.$trans-quick;
@@ -96,11 +85,7 @@
transform: translateX(0.3rem); transform: translateX(0.3rem);
background-color: color.$paper; background-color: color.$paper;
color: color.$ink; color: color.$ink;
box-shadow: 0.12rem 0.15rem 0.5rem rgba(90, 54, 31, 0.3); box-shadow: 0.12rem 0.15rem 0.5rem rgba(20, 14, 9, 0.34), inset -0.2rem 0 0 color.$caramel;
//The one being read is pulled clear of the others, and its paper is clean -
//the dye stays on the edge so the preview text is easiest to read here
background-image:
linear-gradient(to right, rgba(90, 68, 40, 0.22), rgba(90, 68, 40, 0.04) var.$tab-tuck, transparent calc(var.$tab-tuck + 0.5rem));
&:hover, &:hover,
&:focus-visible { &:focus-visible {
@@ -110,11 +95,11 @@
/* The entry still being written */ /* The entry still being written */
.rail__tab--open { .rail__tab--open {
box-shadow: 0.12rem 0.15rem 0.5rem rgba(90, 54, 31, 0.26), inset -0.2rem 0 0 color.$ribbon; box-shadow: 0.12rem 0.15rem 0.5rem rgba(20, 14, 9, 0.34), inset -0.2rem 0 0 color.$ribbon;
} }
.rail__tab--current.rail__tab--open { .rail__tab--current.rail__tab--open {
box-shadow: 0.12rem 0.15rem 0.6rem rgba(90, 54, 31, 0.3), inset -0.2rem 0 0 color.$ribbon; box-shadow: 0.12rem 0.15rem 0.6rem rgba(20, 14, 9, 0.4), inset -0.2rem 0 0 color.$ribbon;
} }
.rail__date { .rail__date {
@@ -153,25 +138,25 @@
.rail__empty { .rail__empty {
font-family: var.$font-hand; font-family: var.$font-hand;
font-size: 1.05rem; font-size: 1.05rem;
color: color.$ink-faint; color: rgba(255, 238, 210, 0.35);
padding: 0.5rem 0.75rem; padding: 0.5rem 0.75rem;
writing-mode: vertical-rl; writing-mode: vertical-rl;
white-space: nowrap; white-space: nowrap;
} }
/* The rail shows a few tabs either side of where the reading is. These two /* "Load older entries" sits at the top of the rail, since the rail is
* say the book carries on past them, and walk the window along. */ * chronological and the oldest entry is the first tab. */
.rail__more { .rail__more {
flex: 0 0 auto; flex: 0 0 auto;
align-self: flex-start; align-self: flex-start;
margin-left: 0; margin-left: 0;
padding: 0.3rem 0.4rem; padding: 0.3rem 0.4rem;
font-size: 0.7rem; font-size: 0.7rem;
color: color.$ink-soft; color: rgba(255, 238, 210, 0.55);
border-radius: var.$block-radius; border-radius: var.$block-radius;
&:hover { &:hover {
color: color.$ink; color: #fff8ea;
background-color: rgba(254, 244, 225, 0.6); background-color: rgba(255, 240, 214, 0.1);
} }
} }
+1 -12
View File
@@ -29,13 +29,7 @@ $tab-height: 3.1rem;
$tab-tuck: 1.4rem; $tab-tuck: 1.4rem;
//Typography //Typography
//The hand the book is written in, chosen in settings and set on :root by $font-hand: "Caveat Variable", "Caveat", "Segoe Script", cursive;
//hands.js. The literal here is only what shows before that runs.
$font-hand: var(--book-hand, "Caveat Variable", "Caveat", "Segoe Script", cursive);
//Type size as a fraction of one ruled line. Travels with the hand, since each
//has its own x-height for the same em.
$hand-scale: var(--book-hand-scale, 0.82);
$font-ui: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; $font-ui: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
//Transitions //Transitions
@@ -43,11 +37,6 @@ $trans-quick: 160ms;
$trans-mid: 280ms; $trans-mid: 280ms;
$trans-slow: 600ms; $trans-slow: 600ms;
//A page turn. Short on purpose: while a leaf is upright both the page it left
//and the page it is landing on are legible at once, and the longer that lasts
//the more it reads as two pages muddled together rather than one turning.
$turn: 430ms;
//Elevation //Elevation
$shadow-page: 0 1.5rem 3rem color.$shadow-deep; $shadow-page: 0 1.5rem 3rem color.$shadow-deep;
$shadow-panel: 0 0.75rem 2rem color.$shadow-deep; $shadow-panel: 0 0.75rem 2rem color.$shadow-deep;
@@ -15,3 +15,4 @@
/* The hand the journal is written in. Only the variable face is pulled in - /* The hand the journal is written in. Only the variable face is pulled in -
* the paginator measures whatever is actually loaded, so the font has to be * the paginator measures whatever is actually loaded, so the font has to be
* settled (document.fonts.ready) before the first layout, not merely linked. */ * settled (document.fonts.ready) before the first layout, not merely linked. */
@import '@fontsource-variable/caveat/index.css';
+2 -2
View File
@@ -65,7 +65,7 @@ export default defineConfig(({ mode }) => {
function myThoughtsLint(isDev) { function myThoughtsLint(isDev) {
return { return {
name: 'daydream-lint', name: 'mythoughts-lint',
apply: 'build', apply: 'build',
//In `--watch` mode Vite re-runs the whole plugin pipeline (including //In `--watch` mode Vite re-runs the whole plugin pipeline (including
//buildStart) on every rebuild it triggers from a file change, so this //buildStart) on every rebuild it triggers from a file change, so this
@@ -96,7 +96,7 @@ async function runLint(isDev) {
function myThoughtsPublicAssets() { function myThoughtsPublicAssets() {
return { return {
name: 'daydream-public-assets', name: 'mythoughts-public-assets',
apply: 'build', apply: 'build',
buildStart() { buildStart() {
cleanGeneratedAssets(); cleanGeneratedAssets();