Change design
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "franzz/mythoughts",
|
"name": "franzz/daydream",
|
||||||
"description": "MyThoughts",
|
"description": "Daydream",
|
||||||
"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\\MyThoughts\\": "lib/",
|
"Franzz\\Daydream\\": "lib/",
|
||||||
"Franzz\\Objects\\": "../objects/inc/"
|
"Franzz\\Objects\\": "../objects/inc/"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "franzz/mythoughts",
|
"name": "franzz/daydream",
|
||||||
"description": "MyThoughts",
|
"description": "Daydream",
|
||||||
"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\\MyThoughts\\": "lib/"
|
"Franzz\\Daydream\\": "lib/"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"config/settings.php"
|
"config/settings.php"
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
# Serve https://localhost/mythoughts/ from the public web root.
|
# Serve https://localhost/daydream/ 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 /mythoughts /var/www/html/mythoughts/public
|
Alias /daydream /var/www/html/daydream/public
|
||||||
|
|
||||||
<Directory /var/www/html/mythoughts/public>
|
<Directory /var/www/html/daydream/public>
|
||||||
Options FollowSymLinks
|
Options FollowSymLinks
|
||||||
AllowOverride None
|
AllowOverride None
|
||||||
Require all granted
|
Require all granted
|
||||||
|
|||||||
@@ -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 = 'mythoughts';
|
public const DB_NAME = 'daydream';
|
||||||
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';
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Franzz\MyThoughts;
|
namespace Franzz\Daydream;
|
||||||
|
|
||||||
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 MyThoughts $oMyThoughts;
|
private Daydream $oDaydream;
|
||||||
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->oMyThoughts = new MyThoughts($sProcessPage, $this->asReq['t']);
|
$this->oDaydream = new Daydream($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 = MyThoughts::getJsonResult(false, MyThoughts::UNAUTHORIZED);
|
if(!$bValidMutationRequest) $sResult = Daydream::getJsonResult(false, Daydream::UNAUTHORIZED);
|
||||||
else $sResult = ($sAction == '') ? $this->oMyThoughts->getAppMainPage($this->getCsrfToken()) : $this->dispatch($sAction);
|
else $sResult = ($sAction == '') ? $this->oDaydream->getAppMainPage($this->getCsrfToken()) : $this->dispatch($sAction);
|
||||||
|
|
||||||
//Clean errors
|
//Clean errors
|
||||||
$sDebug = ob_get_clean();
|
$sDebug = ob_get_clean();
|
||||||
if($sDebug != '') $this->oMyThoughts->addUncaughtError($sDebug);
|
if($sDebug != '') $this->oDaydream->addUncaughtError($sDebug);
|
||||||
$this->closeSession();
|
$this->closeSession();
|
||||||
|
|
||||||
return $sResult;
|
return $sResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function dispatch(string $sAction): string {
|
private function dispatch(string $sAction): string {
|
||||||
$oJournal = $this->oMyThoughts->getJournal();
|
$oJournal = $this->oDaydream->getJournal();
|
||||||
|
|
||||||
return match($sAction) {
|
return match($sAction) {
|
||||||
/* Account */
|
/* Account */
|
||||||
'signup' => $this->oMyThoughts->signup($this->asReq['name'], $this->asReq['email'], $this->asReq['password'], $this->asReq['t']),
|
'signup' => $this->oDaydream->signup($this->asReq['name'], $this->asReq['email'], $this->asReq['password'], $this->asReq['t']),
|
||||||
'login' => $this->oMyThoughts->login($this->asReq['email'], $this->asReq['password'], $this->asReq['t'], $this->asReq['remember']),
|
'login' => $this->oDaydream->login($this->asReq['email'], $this->asReq['password'], $this->asReq['t'], $this->asReq['remember']),
|
||||||
'logout' => $this->oMyThoughts->logout(),
|
'logout' => $this->oDaydream->logout(),
|
||||||
'account' => $this->oMyThoughts->updateAccount($this->asReq['field'], $this->asReq['value']),
|
'account' => $this->oDaydream->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 => MyThoughts::getJsonResult(false, MyThoughts::NOT_FOUND)
|
default => Daydream::getJsonResult(false, Daydream::NOT_FOUND)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Franzz\MyThoughts;
|
namespace Franzz\Daydream;
|
||||||
|
|
||||||
use Franzz\Objects\Db;
|
use Franzz\Objects\Db;
|
||||||
use Franzz\Objects\Main;
|
use Franzz\Objects\Main;
|
||||||
@@ -16,8 +16,8 @@ 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 MyThoughts extends Main {
|
class Daydream extends Main {
|
||||||
public const PROJECT_NAME = 'MyThoughts';
|
public const PROJECT_NAME = 'Daydream';
|
||||||
public const DEFAULT_LANG = 'en';
|
public const DEFAULT_LANG = 'en';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Franzz\MyThoughts;
|
namespace Franzz\Daydream;
|
||||||
|
|
||||||
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 MyThoughts::getJsonResult(false, MyThoughts::UNAUTHORIZED);
|
if(!$this->oUser->isLoggedIn()) return Daydream::getJsonResult(false, Daydream::UNAUTHORIZED);
|
||||||
|
|
||||||
$this->sealStaleEntries();
|
$this->sealStaleEntries();
|
||||||
|
|
||||||
$asEntries = $this->getEntryWindow();
|
$asEntries = $this->getEntryWindow();
|
||||||
|
|
||||||
return MyThoughts::getJsonResult(true, '', [
|
return Daydream::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 MyThoughts::getJsonResult(false, MyThoughts::UNAUTHORIZED);
|
if(!$this->oUser->isLoggedIn()) return Daydream::getJsonResult(false, Daydream::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 MyThoughts::getJsonResult(false, MyThoughts::NOT_FOUND);
|
if($asEntries === null) return Daydream::getJsonResult(false, Daydream::NOT_FOUND);
|
||||||
|
|
||||||
return MyThoughts::getJsonResult(true, '', [
|
return Daydream::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 MyThoughts::getJsonResult(false, MyThoughts::UNAUTHORIZED);
|
if(!$this->oUser->isLoggedIn()) return Daydream::getJsonResult(false, Daydream::UNAUTHORIZED);
|
||||||
|
|
||||||
$oDate = \DateTime::createFromFormat('Y-m-d', $sDate);
|
$oDate = \DateTime::createFromFormat('Y-m-d', $sDate);
|
||||||
if(!$oDate) return MyThoughts::getJsonResult(false, MyThoughts::NOT_FOUND);
|
if(!$oDate) return Daydream::getJsonResult(false, Daydream::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 MyThoughts::getJsonResult(false, 'book.no_entry_yet');
|
if(!$iEntryId) return Daydream::getJsonResult(false, 'book.no_entry_yet');
|
||||||
|
|
||||||
return MyThoughts::getJsonResult(true, '', ['id' => (int) $iEntryId]);
|
return Daydream::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 MyThoughts::getJsonResult(false, MyThoughts::UNAUTHORIZED);
|
if(!$this->oUser->isLoggedIn()) return Daydream::getJsonResult(false, Daydream::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' => MyThoughts::ZERO_TIMESTAMP,
|
'closed_on' => Daydream::ZERO_TIMESTAMP,
|
||||||
'timezone' => date_default_timezone_get()
|
'timezone' => date_default_timezone_get()
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if($iEntryId <= 0) return MyThoughts::getJsonResult(false, 'error.commit_db');
|
if($iEntryId <= 0) return Daydream::getJsonResult(false, 'error.commit_db');
|
||||||
}
|
}
|
||||||
|
|
||||||
return MyThoughts::getJsonResult(true, '', ['entry' => $this->getEntryById($iEntryId)]);
|
return Daydream::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 MyThoughts::getJsonResult(false, MyThoughts::UNAUTHORIZED);
|
if(!$this->oUser->isLoggedIn()) return Daydream::getJsonResult(false, Daydream::UNAUTHORIZED);
|
||||||
if(!$this->ownsEntry($iEntryId)) return MyThoughts::getJsonResult(false, MyThoughts::NOT_FOUND);
|
if(!$this->ownsEntry($iEntryId)) return Daydream::getJsonResult(false, Daydream::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 MyThoughts::getJsonResult(false, 'error.commit_db');
|
return Daydream::getJsonResult(false, 'error.commit_db');
|
||||||
}
|
}
|
||||||
|
|
||||||
return MyThoughts::getJsonResult(true, '', ['id' => $iEntryId, 'saved_at' => time()]);
|
return Daydream::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 MyThoughts::getJsonResult(false, MyThoughts::UNAUTHORIZED);
|
if(!$this->oUser->isLoggedIn()) return Daydream::getJsonResult(false, Daydream::UNAUTHORIZED);
|
||||||
if(!$this->ownsEntry($iEntryId)) return MyThoughts::getJsonResult(false, MyThoughts::NOT_FOUND);
|
if(!$this->ownsEntry($iEntryId)) return Daydream::getJsonResult(false, Daydream::NOT_FOUND);
|
||||||
|
|
||||||
$asResult = $this->sealEntry($iEntryId, $bHasContent ? $sContent : null);
|
$asResult = $this->sealEntry($iEntryId, $bHasContent ? $sContent : null);
|
||||||
return MyThoughts::getJsonResult($asResult['result'], $asResult['desc_lang_id'], $asResult['data']);
|
return Daydream::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 MyThoughts::getResult(true, '', ['id' => $iEntryId, 'discarded' => true]);
|
return Daydream::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 MyThoughts::getResult(false, 'error.commit_db');
|
return Daydream::getResult(false, 'error.commit_db');
|
||||||
}
|
}
|
||||||
|
|
||||||
return MyThoughts::getResult(true, '', ['entry' => $this->getEntryById($iEntryId), 'discarded' => false]);
|
return Daydream::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 MyThoughts::getJsonResult(false, MyThoughts::UNAUTHORIZED);
|
if(!$this->oUser->isLoggedIn()) return Daydream::getJsonResult(false, Daydream::UNAUTHORIZED);
|
||||||
if(!$this->ownsEntry($iEntryId)) return MyThoughts::getJsonResult(false, MyThoughts::NOT_FOUND);
|
if(!$this->ownsEntry($iEntryId)) return Daydream::getJsonResult(false, Daydream::NOT_FOUND);
|
||||||
|
|
||||||
if(!$this->oDb->deleteRow(self::ENTRY_TABLE, $iEntryId)) return MyThoughts::getJsonResult(false, 'error.commit_db');
|
if(!$this->oDb->deleteRow(self::ENTRY_TABLE, $iEntryId)) return Daydream::getJsonResult(false, 'error.commit_db');
|
||||||
|
|
||||||
return MyThoughts::getJsonResult(true, 'book.entry_deleted', ['id' => $iEntryId]);
|
return Daydream::getJsonResult(true, 'book.entry_deleted', ['id' => $iEntryId]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Internals */
|
/* Internals */
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Franzz\MyThoughts;
|
namespace Franzz\Daydream;
|
||||||
|
|
||||||
use Franzz\Objects\Db;
|
use Franzz\Objects\Db;
|
||||||
use Franzz\Objects\PhpObject;
|
use Franzz\Objects\PhpObject;
|
||||||
@@ -26,13 +26,13 @@ class User extends PhpObject {
|
|||||||
'email' => '',
|
'email' => '',
|
||||||
'language' => '',
|
'language' => '',
|
||||||
'timezone' => '',
|
'timezone' => '',
|
||||||
'hand' => MyThoughts::DEFAULT_HAND,
|
'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 = 'mythoughts';
|
private const COOKIE_TOKEN = 'daydream';
|
||||||
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 +101,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 MyThoughts::getResult(false, 'account.name_required');
|
if($sName === '') return Daydream::getResult(false, 'account.name_required');
|
||||||
if(!filter_var($sEmail, FILTER_VALIDATE_EMAIL)) return MyThoughts::getResult(false, 'account.invalid_email');
|
if(!filter_var($sEmail, FILTER_VALIDATE_EMAIL)) return Daydream::getResult(false, 'account.invalid_email');
|
||||||
if(mb_strlen($sPassword) < self::MIN_PASSWORD_LENGTH) return MyThoughts::getResult(false, 'account.password_too_short', [], [self::MIN_PASSWORD_LENGTH]);
|
if(mb_strlen($sPassword) < self::MIN_PASSWORD_LENGTH) return Daydream::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 MyThoughts::getResult(false, 'account.invalid_credentials');
|
return Daydream::getResult(false, 'account.invalid_credentials');
|
||||||
}
|
}
|
||||||
|
|
||||||
$iUserId = $this->oDb->insertRow(self::USER_TABLE, [
|
$iUserId = $this->oDb->insertRow(self::USER_TABLE, [
|
||||||
@@ -117,14 +117,14 @@ 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' => MyThoughts::DEFAULT_HAND,
|
'hand' => Daydream::DEFAULT_HAND,
|
||||||
'clearance' => self::CLEARANCE_USER
|
'clearance' => self::CLEARANCE_USER
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if($iUserId <= 0) return MyThoughts::getResult(false, 'error.commit_db');
|
if($iUserId <= 0) return Daydream::getResult(false, 'error.commit_db');
|
||||||
|
|
||||||
$this->openSessionFor($iUserId, true);
|
$this->openSessionFor($iUserId, true);
|
||||||
return MyThoughts::getResult(true, 'account.welcome', ['user' => $this->getUserInfo()]);
|
return Daydream::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 +142,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 MyThoughts::getResult(false, 'account.invalid_credentials');
|
if($iUserId <= 0 || !$bValid) return Daydream::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 MyThoughts::getResult(true, 'account.logged_in', ['user' => $this->getUserInfo()]);
|
return Daydream::getResult(true, 'account.logged_in', ['user' => $this->getUserInfo()]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function logout(): array {
|
public function logout(): array {
|
||||||
@@ -159,24 +159,24 @@ 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 MyThoughts::getResult(true, 'account.logged_out');
|
return Daydream::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 MyThoughts::getResult(false, MyThoughts::UNAUTHORIZED);
|
if(!$this->isLoggedIn()) return Daydream::getResult(false, Daydream::UNAUTHORIZED);
|
||||||
if(!in_array($sField, ['name', 'language', 'timezone', 'hand'], true)) return MyThoughts::getResult(false, MyThoughts::NOT_FOUND);
|
if(!in_array($sField, ['name', 'language', 'timezone', 'hand'], true)) return Daydream::getResult(false, Daydream::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 MyThoughts::getResult(false, 'account.name_required');
|
if($sField === 'name' && $sValue === '') return Daydream::getResult(false, 'account.name_required');
|
||||||
if($sField === 'timezone' && !in_array($sValue, \DateTimeZone::listIdentifiers(), true)) return MyThoughts::getResult(false, MyThoughts::NOT_FOUND);
|
if($sField === 'timezone' && !in_array($sValue, \DateTimeZone::listIdentifiers(), true)) return Daydream::getResult(false, Daydream::NOT_FOUND);
|
||||||
if($sField === 'hand' && !in_array($sValue, MyThoughts::getHandIds(), 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 MyThoughts::getResult(false, 'error.commit_db');
|
return Daydream::getResult(false, 'error.commit_db');
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->setUserId($this->iUserId);
|
$this->setUserId($this->iUserId);
|
||||||
return MyThoughts::getResult(true, 'account.saved', ['user' => $this->getUserInfo()]);
|
return Daydream::getResult(true, 'account.saved', ['user' => $this->getUserInfo()]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Session plumbing */
|
/* Session plumbing */
|
||||||
@@ -255,7 +255,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' => MyThoughts::ZERO_TIMESTAMP]);
|
$this->oDb->updateRow(self::USER_TABLE, $this->iUserId, ['token' => '', 'token_exp' => Daydream::ZERO_TIMESTAMP]);
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->writeCookie('', time() - 3600);
|
$this->writeCookie('', time() - 3600);
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"name": "mythoughts",
|
"name": "daydream",
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "mythoughts",
|
"name": "daydream",
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fontsource-variable/caveat": "^5.3.0",
|
"@fontsource-variable/caveat": "^5.3.0",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "mythoughts",
|
"name": "daydream",
|
||||||
"description": "A journal that behaves like an open book: write the left page, then the right, then turn.",
|
"description": "a place for wandering thoughts.",
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -2,6 +2,6 @@
|
|||||||
|
|
||||||
require __DIR__.'/../vendor/autoload.php';
|
require __DIR__.'/../vendor/autoload.php';
|
||||||
|
|
||||||
use Franzz\MyThoughts\Controller;
|
use Franzz\Daydream\Controller;
|
||||||
|
|
||||||
echo (new Controller())->handle(__FILE__, $argv ?? []);
|
echo (new Controller())->handle(__FILE__, $argv ?? []);
|
||||||
|
|||||||
@@ -36,8 +36,8 @@
|
|||||||
"hand_sample": "The morning came"
|
"hand_sample": "The morning came"
|
||||||
},
|
},
|
||||||
"book": {
|
"book": {
|
||||||
"title": "MyThoughts",
|
"title": "Daydream",
|
||||||
"tagline": "Your thoughts, on paper.",
|
"tagline": "a place for wandering thoughts.",
|
||||||
"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.",
|
||||||
|
|||||||
@@ -36,8 +36,8 @@
|
|||||||
"hand_sample": "Le matin est venu"
|
"hand_sample": "Le matin est venu"
|
||||||
},
|
},
|
||||||
"book": {
|
"book": {
|
||||||
"title": "MyThoughts",
|
"title": "Daydream",
|
||||||
"tagline": "Vos pensées, sur le papier.",
|
"tagline": "Où l'esprit vagabonde.",
|
||||||
"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.",
|
||||||
|
|||||||
@@ -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="#2c2114">
|
<meta name="theme-color" content="#fef9ed">
|
||||||
<meta name="apple-mobile-web-app-title" content="[#]title[#]" />
|
<meta name="apple-mobile-web-app-title" content="[#]title[#]" />
|
||||||
<link rel="icon" type="image/svg+xml" href="assets/images/icons/favicon.svg" />
|
<link rel="icon" type="image/png" href="assets/images/icons/favicon.png" />
|
||||||
<link rel="apple-touch-icon" href="assets/images/icons/apple-touch-icon.svg" />
|
<link rel="apple-touch-icon" href="assets/images/icons/apple-touch-icon.png" />
|
||||||
<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>
|
||||||
|
|||||||
@@ -184,8 +184,12 @@ 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="consts.title" />
|
<img class="app__logo" :src="sLogo" alt="" />
|
||||||
|
<span class="app__name">
|
||||||
|
<!-- 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 class="app__tagline">{{ lang.get('book.tagline') }}</span>
|
||||||
|
</span>
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<div class="app__tools">
|
<div class="app__tools">
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { createApp, reactive } from 'vue';
|
|||||||
import App from './App.vue';
|
import App from './App.vue';
|
||||||
|
|
||||||
//Style
|
//Style
|
||||||
import '@styles/mythoughts.scss';
|
import '@styles/daydream.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);
|
||||||
|
|||||||
@@ -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 logo's own colours, as it was drawn -->
|
<!-- Paper at last: the mark as it was painted, on paper of its own -->
|
||||||
<img class="leaf__logo" :src="sLogo" alt="MyThoughts" />
|
<img class="leaf__logo" :src="sLogo" alt="Daydream" />
|
||||||
<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>
|
||||||
|
|||||||
@@ -1,6 +1,33 @@
|
|||||||
<script>
|
<script>
|
||||||
import appIcon from '@components/AppIcon';
|
import appIcon from '@components/AppIcon';
|
||||||
import { formatShortDate, formatTime } from '@scripts/time';
|
import { formatShortDate, formatTime, getDayKey } 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
|
||||||
@@ -36,7 +63,8 @@ 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))
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
@@ -125,6 +153,7 @@ 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">
|
||||||
|
|||||||
|
After Width: | Height: | Size: 16 KiB |
@@ -1,23 +0,0 @@
|
|||||||
<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>
|
|
||||||
|
Before Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 12 KiB |
@@ -1,17 +0,0 @@
|
|||||||
<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>
|
|
||||||
|
Before Width: | Height: | Size: 903 B |
@@ -1,23 +1,23 @@
|
|||||||
{
|
{
|
||||||
"name": "MyThoughts",
|
"name": "Daydream",
|
||||||
"short_name": "MyThoughts",
|
"short_name": "Daydream",
|
||||||
"description": "A quiet place to write. Your thoughts, on paper.",
|
"description": "A quiet place to write. Where the mind wanders, on paper.",
|
||||||
"start_url": "../../../",
|
"start_url": "../../../",
|
||||||
"scope": "../../../",
|
"scope": "../../../",
|
||||||
"display": "standalone",
|
"display": "standalone",
|
||||||
"orientation": "any",
|
"orientation": "any",
|
||||||
"background_color": "#2c2114",
|
"background_color": "#fef9ed",
|
||||||
"theme_color": "#2c2114",
|
"theme_color": "#fef9ed",
|
||||||
"icons": [
|
"icons": [
|
||||||
{
|
{
|
||||||
"src": "favicon.svg",
|
"src": "favicon.png",
|
||||||
"sizes": "any",
|
"sizes": "128x128",
|
||||||
"type": "image/svg+xml"
|
"type": "image/png"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"src": "apple-touch-icon.svg",
|
"src": "apple-touch-icon.png",
|
||||||
"sizes": "any",
|
"sizes": "180x180",
|
||||||
"type": "image/svg+xml",
|
"type": "image/png",
|
||||||
"purpose": "maskable"
|
"purpose": "maskable"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
|
After Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 298 KiB |
@@ -5,7 +5,7 @@
|
|||||||
* all six is a few KB of CSS - the woff2 files are fetched only for the hand
|
* 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.
|
* actually in use, plus whatever the settings picker previews when it is opened.
|
||||||
*
|
*
|
||||||
* The list itself lives in MyThoughts::HANDS on the server, which is also what
|
* 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.
|
* validates a change - adding one there means adding its import here.
|
||||||
*/
|
*/
|
||||||
import '@fontsource-variable/caveat/index.css';
|
import '@fontsource-variable/caveat/index.css';
|
||||||
|
|||||||
@@ -31,30 +31,44 @@
|
|||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* The logo is dark roast ink on a transparent bubble - drawn for paper, not for
|
/* The mark is the painting itself, carried as a layer over its own sheet: what
|
||||||
* the desk. Rather than sit it on a light patch and break the surface, it is
|
* each pixel took away from the paper is its opacity, so the book arrives with
|
||||||
* re-inked in cream: brightness(0) flattens it to a silhouette, invert lifts it
|
* its colours and its shadow arrives as a shadow. It needs no plate behind it
|
||||||
* to white, and the sepia/hue pass warms that back to the colour of the pages.
|
* and no shadow added to it - it darkens the desk here and the paper of the
|
||||||
* Its full colour is kept for the sign-in leaf, which is paper. */
|
* sign-in card by exactly as much as it darkened the sheet it was painted on. */
|
||||||
.app__logo {
|
.app__logo {
|
||||||
height: 3.3rem;
|
//Below about 80px wide the ribbons stop reading as ribbons, so the mark is
|
||||||
|
//given the room it needs rather than shrunk to fit
|
||||||
|
height: 3.5rem;
|
||||||
width: auto;
|
width: auto;
|
||||||
display: block;
|
display: block;
|
||||||
//brightness(0) flattens the artwork to a silhouette, invert lifts it to
|
flex: 0 0 auto;
|
||||||
//white, and the short sepia pass warms that to the colour of the pages.
|
}
|
||||||
//Kept deliberately simple - a longer chain only muddies it.
|
|
||||||
filter:
|
.app__name {
|
||||||
brightness(0)
|
display: flex;
|
||||||
invert(1)
|
flex-direction: column;
|
||||||
sepia(0.22)
|
justify-content: center;
|
||||||
saturate(1.5)
|
min-width: 0;
|
||||||
drop-shadow(0 1px 1px rgba(0, 0, 0, 0.4));
|
line-height: 1.05;
|
||||||
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: rgba(255, 238, 210, 0.58);
|
color: color.$ink-soft;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
@@ -77,7 +91,9 @@
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 0;
|
gap: 0;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
padding: 0 clamp(0.5rem, 2vw, 2rem) clamp(0.75rem, 2.5vh, 2rem);
|
//Room for the desk. The book is still the app, but a book with no desk
|
||||||
|
//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 {
|
||||||
@@ -128,21 +144,23 @@
|
|||||||
padding: 0.5rem 0.9rem;
|
padding: 0.5rem 0.9rem;
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
color: #fff4e2;
|
//The one thing that is allowed to be dark on the desk: the cover of the
|
||||||
background-color: rgba(29, 21, 15, 0.92);
|
//book, borrowed for a strip that has to be read before it goes away
|
||||||
|
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.$ribbon-deep;
|
background-color: color.$bad;
|
||||||
}
|
}
|
||||||
|
|
||||||
.app__notice-close {
|
.app__notice-close {
|
||||||
color: rgba(255, 244, 226, 0.6);
|
color: rgba(254, 244, 225, 0.6);
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: #fff4e2;
|
color: color.$paper;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -38,7 +38,14 @@
|
|||||||
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;
|
||||||
@@ -53,7 +60,10 @@
|
|||||||
color.$paper-edge 62%,
|
color.$paper-edge 62%,
|
||||||
color.$paper-deep 100%
|
color.$paper-deep 100%
|
||||||
);
|
);
|
||||||
box-shadow: 0 0.9rem 1.6rem color.$shadow-deep;
|
box-shadow:
|
||||||
|
0 0 0 0.5rem color.$cover,
|
||||||
|
0 0 0 calc(0.5rem + 1px) color.$cover-deep,
|
||||||
|
0 1rem 1.8rem color.$shadow-deep;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -690,8 +700,8 @@
|
|||||||
.book__ribbon {
|
.book__ribbon {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: calc(50% - 0.55rem);
|
left: calc(50% - 0.425rem);
|
||||||
width: 1.1rem;
|
width: 0.85rem;
|
||||||
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);
|
||||||
|
|||||||
@@ -105,11 +105,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.calendar__day--today {
|
.calendar__day--today {
|
||||||
box-shadow: inset 0 0 0 1px color.$caramel;
|
box-shadow: inset 0 0 0 1px color.$amber;
|
||||||
}
|
}
|
||||||
|
|
||||||
.calendar__day--current {
|
.calendar__day--current {
|
||||||
color: #fff6e6;
|
color: color.$paper;
|
||||||
background-color: color.$ribbon;
|
background-color: color.$ribbon;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
@@ -117,7 +117,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&::after {
|
&::after {
|
||||||
background-color: #fff6e6;
|
background-color: color.$paper;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,52 +1,87 @@
|
|||||||
/* The palette is taken from the logo, not chosen alongside it.
|
/* Everything here is sampled off the logo.
|
||||||
*
|
*
|
||||||
* src/images/logo.png is dark-roast lettering in a thought bubble with a cup of
|
* The mark is an open book lying on warm ivory: a dark brown board, cream
|
||||||
* coffee; sampling it gives one dominant near-black brown (#2c2114) and a run of
|
* pages, a hand you cannot quite read, and three dyed ribbons hanging out of
|
||||||
* caramels from #795731 up to #b47e41. Those are the colours below. The book is
|
* the fore-edge. Every value below was read out of that drawing rather than
|
||||||
* the same idea in furniture: a coffee-dark desk, cream paper, and caramel for
|
* guessed next to it - the sheet it is printed on is the desk, the pages are
|
||||||
* anything that has to catch the eye.
|
* the paper, the board is the ink, and the three ribbons are the only colour
|
||||||
|
* the app is allowed to raise its voice with.
|
||||||
*
|
*
|
||||||
* There is no pure grey and no pure black anywhere - paper and lamplight do not
|
* There is no pure grey, no pure black and no blue anywhere, because there is
|
||||||
* have any, and neither does the logo. */
|
* none in the mark. Every neutral here is a brown that has been let go.
|
||||||
|
*
|
||||||
|
* 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 book lies on - straight from the logo's darkest inks
|
//The desk: the sheet the mark is printed on, the light on it, and the corners
|
||||||
$desk-deep: #1a140d;
|
//falling away into the shadow the book throws
|
||||||
$desk: #2c2114;
|
$linen: #fef9ed;
|
||||||
$desk-edge: #44321e;
|
$linen-light: #fffdf7;
|
||||||
|
$linen-deep: #f2e7d1;
|
||||||
|
|
||||||
//Paper. -shade is the tint towards the spine, -edge the cut edge of the stack
|
//Paper. -shade is the tint towards the spine, -edge and -deep the cut edges of
|
||||||
$paper: #f8f2e4;
|
//the stack, lit and in shadow
|
||||||
$paper-shade: #f0e7d3;
|
$paper: #fef4e1;
|
||||||
$paper-edge: #e0d3b8;
|
$paper-shade: #fbf1dc;
|
||||||
$paper-deep: #cdbb99;
|
$paper-edge: #eed9b7;
|
||||||
|
$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
|
//about it, and what is barely there. The heaviest is the brown the wordmark is
|
||||||
$ink: #2e2822;
|
//set in, the middle one is the lit face of the board, and the faintest is the
|
||||||
$ink-soft: #6d6358;
|
//hand the mark is written in
|
||||||
$ink-faint: #a29684;
|
$ink: #45220d;
|
||||||
$ink-ghost: rgba(46, 40, 34, 0.38);
|
$ink-soft: #774b31;
|
||||||
|
$ink-faint: #a9957b;
|
||||||
|
$ink-ghost: rgba(69, 34, 13, 0.32);
|
||||||
|
|
||||||
//Ruling. The horizontal rules are cold on purpose - they are the one thing on
|
//Ruling. Not ink: the rules are that same handwriting brown, faded until they
|
||||||
//the page that is not ink, and that contrast is what makes paper read as paper
|
//are something the writing crosses rather than meets
|
||||||
$rule: rgba(90, 120, 150, 0.22);
|
$rule: rgba(169, 149, 123, 0.42);
|
||||||
$rule-margin: rgba(168, 118, 62, 0.5);
|
$rule-margin: rgba(223, 167, 91, 0.6);
|
||||||
|
|
||||||
//Accents: the coffee and the steam swirl
|
//The gold ribbon: anything that has to catch the eye off the paper
|
||||||
$caramel: #b47e41;
|
$amber: #eab469;
|
||||||
$caramel-deep: #795731;
|
$amber-light: #f0bd74;
|
||||||
$caramel-light: #d0a06a;
|
$amber-deep: #9a6316;
|
||||||
|
|
||||||
//The ribbon marking the page being written on
|
//The green ribbon
|
||||||
$ribbon: $caramel-deep;
|
$sage: #a2b097;
|
||||||
$ribbon-deep: #5c4428;
|
$sage-deep: #647458;
|
||||||
|
|
||||||
//Feedback. Brick rather than red, so a warning still belongs to the palette
|
//The lilac ribbon. It marks the page being written on - gold and green already
|
||||||
$ok: #5c6b3a;
|
//have work, and this is the one the eye has nothing else to confuse with
|
||||||
$warn: #a8763e;
|
$ribbon: #8e5777;
|
||||||
$bad: #9c4a34;
|
$ribbon-deep: #6f445c;
|
||||||
|
|
||||||
//Overlays
|
/* Dyed index tabs.
|
||||||
$veil: rgba(20, 14, 9, 0.74);
|
*
|
||||||
$shadow-soft: rgba(20, 14, 9, 0.18);
|
* Six, cycled by the day an entry was written - so every entry from one day
|
||||||
$shadow-deep: rgba(20, 14, 9, 0.45);
|
* shares a colour and the rail reads as a run of days rather than a list of
|
||||||
|
* 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);
|
||||||
|
|||||||
@@ -15,19 +15,25 @@ 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.$desk-deep;
|
background-color: color.$linen;
|
||||||
|
|
||||||
//The desk: a warm pool of lamplight falling on wood, with the grain done as
|
/* The desk the mark is printed on: warm ivory, lit where the book lies.
|
||||||
//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:
|
||||||
radial-gradient(ellipse 120% 90% at 50% -10%, rgba(255, 226, 178, 0.16), transparent 60%),
|
//The light the book lies in
|
||||||
repeating-linear-gradient(
|
radial-gradient(ellipse 62% 52% at 50% 40%, #{color.$linen-light} 0%, rgba(255, 253, 247, 0) 72%),
|
||||||
92deg,
|
|
||||||
rgba(0, 0, 0, 0.05) 0 3px,
|
//The warmth of that light, carried further out
|
||||||
rgba(255, 255, 255, 0.014) 3px 7px,
|
radial-gradient(ellipse 96% 78% at 50% 46%, rgba(255, 253, 247, 0.6) 0%, rgba(255, 253, 247, 0) 78%),
|
||||||
rgba(0, 0, 0, 0.035) 7px 11px
|
|
||||||
),
|
//The corners of the desk, where the light does not reach
|
||||||
linear-gradient(160deg, color.$desk-edge 0%, color.$desk 45%, color.$desk-deep 100%);
|
radial-gradient(ellipse 118% 96% at 50% 44%, rgba(242, 231, 209, 0) 58%, #{color.$linen-deep} 100%);
|
||||||
background-attachment: fixed;
|
background-attachment: fixed;
|
||||||
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@@ -55,34 +61,35 @@ 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.$caramel-deep;
|
accent-color: color.$amber-deep;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Buttons that sit on the desk rather than on the paper: brass-ish, restrained */
|
/* Buttons that sit on the desk rather than on the paper: a card of the same
|
||||||
|
* 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: rgba(255, 244, 224, 0.82);
|
color: color.$ink-soft;
|
||||||
background-color: rgba(255, 240, 214, 0.07);
|
background-color: rgba(254, 244, 225, 0.55);
|
||||||
border: 1px solid rgba(255, 240, 214, 0.14);
|
border: 1px solid color.$paper-edge;
|
||||||
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: #fff8ea;
|
color: color.$ink;
|
||||||
background-color: rgba(255, 240, 214, 0.14);
|
background-color: color.$paper;
|
||||||
border-color: rgba(255, 240, 214, 0.28);
|
border-color: color.$paper-deep;
|
||||||
}
|
}
|
||||||
|
|
||||||
&[aria-expanded="true"],
|
&[aria-expanded="true"],
|
||||||
&.is-active {
|
&.is-active {
|
||||||
color: #fff8ea;
|
color: color.$ink;
|
||||||
background-color: rgba(255, 240, 214, 0.18);
|
background-color: color.$paper-shade;
|
||||||
border-color: rgba(255, 240, 214, 0.34);
|
border-color: color.$paper-deep;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:disabled {
|
&:disabled {
|
||||||
@@ -109,7 +116,7 @@ textarea {
|
|||||||
&:focus {
|
&:focus {
|
||||||
outline: 0;
|
outline: 0;
|
||||||
border-color: color.$ribbon;
|
border-color: color.$ribbon;
|
||||||
box-shadow: 0 0 0 3px rgba(163, 55, 47, 0.14);
|
box-shadow: 0 0 0 3px rgba(142, 87, 119, 0.18);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,7 +143,7 @@ textarea {
|
|||||||
}
|
}
|
||||||
|
|
||||||
:focus-visible {
|
:focus-visible {
|
||||||
outline: 2px solid color.$caramel;
|
outline: 2px solid color.$amber;
|
||||||
outline-offset: 2px;
|
outline-offset: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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(29, 21, 15, 0.96);
|
background-color: rgba(54, 29, 12, 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,8 +158,21 @@
|
|||||||
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 {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
@use "sass:color" as sass-color;
|
||||||
@use "@styles/color";
|
@use "@styles/color";
|
||||||
@use "@styles/var";
|
@use "@styles/var";
|
||||||
|
|
||||||
@@ -102,14 +103,17 @@
|
|||||||
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;
|
||||||
color: #fff6e6;
|
//The gold ribbon with the wordmark brown on it, exactly as the two sit
|
||||||
background-color: color.$ribbon;
|
//together in the mark. Gold is the palette's one colour for things that
|
||||||
|
//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: color.$ribbon-deep;
|
background-color: sass-color.adjust(color.$amber, $lightness: 6%);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:disabled {
|
&:disabled {
|
||||||
@@ -132,12 +136,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.text-button--bad {
|
.text-button--bad {
|
||||||
color: color.$ribbon;
|
color: color.$bad;
|
||||||
|
|
||||||
&:hover,
|
&:hover,
|
||||||
&:focus-visible {
|
&:focus-visible {
|
||||||
color: color.$ribbon-deep;
|
color: color.$bad;
|
||||||
background-color: rgba(163, 55, 47, 0.08);
|
background-color: rgba(155, 70, 49, 0.09);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,7 +152,7 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.4rem;
|
gap: 0.4rem;
|
||||||
font-size: 0.78rem;
|
font-size: 0.78rem;
|
||||||
color: rgba(255, 238, 210, 0.5);
|
color: color.$ink-faint;
|
||||||
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;
|
||||||
@@ -156,11 +160,11 @@
|
|||||||
|
|
||||||
.saver--saving,
|
.saver--saving,
|
||||||
.saver--pending {
|
.saver--pending {
|
||||||
color: rgba(255, 238, 210, 0.75);
|
color: color.$ink-soft;
|
||||||
}
|
}
|
||||||
|
|
||||||
.saver--failed {
|
.saver--failed {
|
||||||
color: #f0b6ae;
|
color: color.$bad;
|
||||||
}
|
}
|
||||||
|
|
||||||
.saver__dot {
|
.saver__dot {
|
||||||
@@ -226,9 +230,9 @@
|
|||||||
|
|
||||||
//The one in use: a ruled line under the sample, like the page itself
|
//The one in use: a ruled line under the sample, like the page itself
|
||||||
.hand-picker__option--on {
|
.hand-picker__option--on {
|
||||||
border-color: color.$caramel;
|
border-color: color.$amber;
|
||||||
background-color: color.$paper-shade;
|
background-color: color.$paper-shade;
|
||||||
box-shadow: inset 0 0 0 1px color.$caramel;
|
box-shadow: inset 0 0 0 1px color.$amber;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hand-picker__sample {
|
.hand-picker__sample {
|
||||||
|
|||||||
@@ -69,9 +69,17 @@
|
|||||||
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: 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));
|
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)),
|
||||||
|
//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(20, 14, 9, 0.3);
|
box-shadow: 0.12rem 0.15rem 0.5rem rgba(90, 54, 31, 0.22);
|
||||||
|
|
||||||
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;
|
||||||
|
|
||||||
@@ -88,7 +96,11 @@
|
|||||||
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(20, 14, 9, 0.34), inset -0.2rem 0 0 color.$caramel;
|
box-shadow: 0.12rem 0.15rem 0.5rem rgba(90, 54, 31, 0.3);
|
||||||
|
//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 {
|
||||||
@@ -98,11 +110,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(20, 14, 9, 0.34), inset -0.2rem 0 0 color.$ribbon;
|
box-shadow: 0.12rem 0.15rem 0.5rem rgba(90, 54, 31, 0.26), 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(20, 14, 9, 0.4), inset -0.2rem 0 0 color.$ribbon;
|
box-shadow: 0.12rem 0.15rem 0.6rem rgba(90, 54, 31, 0.3), inset -0.2rem 0 0 color.$ribbon;
|
||||||
}
|
}
|
||||||
|
|
||||||
.rail__date {
|
.rail__date {
|
||||||
@@ -141,7 +153,7 @@
|
|||||||
.rail__empty {
|
.rail__empty {
|
||||||
font-family: var.$font-hand;
|
font-family: var.$font-hand;
|
||||||
font-size: 1.05rem;
|
font-size: 1.05rem;
|
||||||
color: rgba(255, 238, 210, 0.35);
|
color: color.$ink-faint;
|
||||||
padding: 0.5rem 0.75rem;
|
padding: 0.5rem 0.75rem;
|
||||||
writing-mode: vertical-rl;
|
writing-mode: vertical-rl;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
@@ -155,11 +167,11 @@
|
|||||||
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: rgba(255, 238, 210, 0.55);
|
color: color.$ink-soft;
|
||||||
border-radius: var.$block-radius;
|
border-radius: var.$block-radius;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: #fff8ea;
|
color: color.$ink;
|
||||||
background-color: rgba(255, 240, 214, 0.1);
|
background-color: rgba(254, 244, 225, 0.6);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ export default defineConfig(({ mode }) => {
|
|||||||
|
|
||||||
function myThoughtsLint(isDev) {
|
function myThoughtsLint(isDev) {
|
||||||
return {
|
return {
|
||||||
name: 'mythoughts-lint',
|
name: 'daydream-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: 'mythoughts-public-assets',
|
name: 'daydream-public-assets',
|
||||||
apply: 'build',
|
apply: 'build',
|
||||||
buildStart() {
|
buildStart() {
|
||||||
cleanGeneratedAssets();
|
cleanGeneratedAssets();
|
||||||
|
|||||||