Merge branch 'style-foundation'

This commit is contained in:
Yisroel Baum 2026-05-01 11:37:46 +03:00
commit a9a7461aad
Signed by: yisroelbaum
GPG key ID: 0FA60884F75520A9
12 changed files with 727 additions and 78 deletions

527
public/css/app.css Normal file
View file

@ -0,0 +1,527 @@
/* =========================================================================
Daily Goals - app.css
Single shared stylesheet. Sectioned for readability.
========================================================================= */
/* -------------------------------------------------------------------------
1. Reset
------------------------------------------------------------------------- */
*,
*::before,
*::after {
box-sizing: border-box;
}
html,
body {
margin: 0;
padding: 0;
}
img,
svg {
display: block;
max-width: 100%;
}
button {
font: inherit;
color: inherit;
cursor: pointer;
}
input,
textarea,
select {
font: inherit;
color: inherit;
}
/* -------------------------------------------------------------------------
2. Design tokens
------------------------------------------------------------------------- */
:root {
/* Color: warm, readable palette */
--color-bg: #faf7f2;
--color-surface: #ffffff;
--color-surface-alt: #f3ede2;
--color-text: #2b2622;
--color-text-muted: #6b5d52;
--color-primary: #5a7a4f;
--color-primary-hover: #486340;
--color-accent: #8a5a3b;
--color-accent-hover: #6f4730;
--color-border: #e3dccf;
--color-border-strong: #c9bfae;
--color-danger: #a94442;
--color-success: #4f7a5a;
/* Typography */
--font-serif: Georgia, "Iowan Old Style", "Source Serif Pro", serif;
--font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui,
sans-serif;
--font-size-base: 1rem;
--font-size-sm: 0.875rem;
--font-size-lg: 1.125rem;
--font-size-h1: 2rem;
--font-size-h2: 1.5rem;
--font-size-h3: 1.25rem;
--line-height-base: 1.6;
--line-height-tight: 1.25;
/* Spacing scale */
--space-1: 0.25rem;
--space-2: 0.5rem;
--space-3: 0.75rem;
--space-4: 1rem;
--space-5: 1.5rem;
--space-6: 2rem;
--space-7: 3rem;
/* Layout */
--max-width: 42rem;
--max-width-wide: 60rem;
/* Radii */
--radius-sm: 4px;
--radius-md: 8px;
--radius-lg: 12px;
/* Shadows */
--shadow-sm: 0 1px 2px rgba(43, 38, 34, 0.06);
--shadow-md: 0 4px 12px rgba(43, 38, 34, 0.1);
--shadow-lg: 0 12px 32px rgba(43, 38, 34, 0.18);
/* Transitions */
--transition-fast: 120ms ease;
--transition-base: 200ms ease;
}
/* -------------------------------------------------------------------------
3. Base elements
------------------------------------------------------------------------- */
body {
background-color: var(--color-bg);
color: var(--color-text);
font-family: var(--font-sans);
font-size: var(--font-size-base);
line-height: var(--line-height-base);
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
}
h1,
h2,
h3,
h4,
h5,
h6 {
font-family: var(--font-serif);
font-weight: 600;
line-height: var(--line-height-tight);
margin: 0;
color: var(--color-text);
}
h1 {
font-size: var(--font-size-h1);
}
h2 {
font-size: var(--font-size-h2);
}
h3 {
font-size: var(--font-size-h3);
}
p {
margin: 0;
}
a {
color: var(--color-accent);
text-decoration: none;
transition: color var(--transition-fast);
}
a:hover,
a:focus {
color: var(--color-accent-hover);
text-decoration: underline;
}
ul,
ol {
margin: 0;
padding: 0;
list-style: none;
}
label {
display: block;
font-size: var(--font-size-sm);
color: var(--color-text-muted);
font-weight: 500;
}
input[type="text"],
input[type="email"],
input[type="password"],
input[type="date"],
input[type="number"],
textarea,
select {
display: block;
width: 100%;
margin-top: var(--space-1);
padding: var(--space-2) var(--space-3);
background-color: var(--color-surface);
color: var(--color-text);
border: 1px solid var(--color-border-strong);
border-radius: var(--radius-sm);
transition: border-color var(--transition-fast),
box-shadow var(--transition-fast);
}
input:focus,
textarea:focus,
select:focus {
outline: none;
border-color: var(--color-primary);
box-shadow: 0 0 0 3px rgba(90, 122, 79, 0.15);
}
button {
display: inline-block;
padding: var(--space-2) var(--space-4);
background-color: var(--color-surface);
color: var(--color-text);
border: 1px solid var(--color-border-strong);
border-radius: var(--radius-sm);
font-weight: 500;
transition: background-color var(--transition-fast),
border-color var(--transition-fast), color var(--transition-fast);
}
button:hover,
button:focus {
background-color: var(--color-surface-alt);
border-color: var(--color-text-muted);
}
form {
margin: 0;
}
/* -------------------------------------------------------------------------
4. Layout primitives
------------------------------------------------------------------------- */
.container {
width: 100%;
max-width: var(--max-width);
margin-inline: auto;
padding: var(--space-5) var(--space-4);
}
.container-wide {
max-width: var(--max-width-wide);
}
.stack > * + * {
margin-top: var(--space-4);
}
.stack-sm > * + * {
margin-top: var(--space-2);
}
.stack-lg > * + * {
margin-top: var(--space-6);
}
.cluster {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: var(--space-3);
}
.cluster-end {
justify-content: flex-end;
}
.cluster-between {
justify-content: space-between;
}
.center {
display: flex;
align-items: center;
justify-content: center;
}
/* -------------------------------------------------------------------------
5. Components
------------------------------------------------------------------------- */
.site-header {
border-bottom: 1px solid var(--color-border);
padding: var(--space-3) var(--space-4);
background-color: var(--color-surface);
}
.site-header .site-header-inner {
width: 100%;
max-width: var(--max-width);
margin-inline: auto;
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
gap: var(--space-3);
}
.btn {
display: inline-block;
padding: var(--space-2) var(--space-4);
border-radius: var(--radius-sm);
border: 1px solid var(--color-border-strong);
background-color: var(--color-surface);
color: var(--color-text);
font-weight: 500;
text-decoration: none;
transition: background-color var(--transition-fast),
border-color var(--transition-fast), color var(--transition-fast);
}
.btn:hover,
.btn:focus {
background-color: var(--color-surface-alt);
border-color: var(--color-text-muted);
text-decoration: none;
}
.btn-primary {
background-color: var(--color-primary);
border-color: var(--color-primary);
color: #ffffff;
}
.btn-primary:hover,
.btn-primary:focus {
background-color: var(--color-primary-hover);
border-color: var(--color-primary-hover);
color: #ffffff;
}
.btn-secondary {
background-color: transparent;
border-color: var(--color-border-strong);
color: var(--color-text);
}
.btn-danger {
background-color: transparent;
border-color: var(--color-danger);
color: var(--color-danger);
}
.btn-danger:hover,
.btn-danger:focus {
background-color: var(--color-danger);
color: #ffffff;
}
.card {
background-color: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
padding: var(--space-4);
box-shadow: var(--shadow-sm);
}
.card-link {
display: block;
color: inherit;
text-decoration: none;
transition: border-color var(--transition-fast),
box-shadow var(--transition-fast),
transform var(--transition-fast);
}
.card-link:hover,
.card-link:focus {
border-color: var(--color-border-strong);
box-shadow: var(--shadow-md);
text-decoration: none;
}
.list-cards {
display: flex;
flex-direction: column;
gap: var(--space-3);
}
.modal {
position: fixed;
inset: 0;
z-index: 50;
display: flex;
align-items: center;
justify-content: center;
padding: var(--space-4);
background-color: rgba(43, 38, 34, 0.45);
}
.modal[hidden] {
display: none;
}
.modal-content {
width: 100%;
max-width: 28rem;
background-color: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
box-shadow: var(--shadow-lg);
padding: var(--space-5);
}
.error {
color: var(--color-danger);
font-size: var(--font-size-sm);
}
.auth-shell {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: var(--space-5) var(--space-4);
}
.auth-card {
width: 100%;
max-width: 24rem;
background-color: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
box-shadow: var(--shadow-md);
padding: var(--space-6);
}
.muted {
color: var(--color-text-muted);
}
/* -------------------------------------------------------------------------
6. Utilities
------------------------------------------------------------------------- */
.full-width {
width: 100%;
}
.text-center {
text-align: center;
}
[hidden] {
display: none !important;
}
/* -------------------------------------------------------------------------
7. Page-specific: node tree (text detail)
-------------------------------------------------------------------------
The tree DOM is rendered as plain ul/li with buttons and inputs as
direct children of each li. Cypress assertions rely on this exact
structure, so we style it via descendant selectors only - no wrapper
elements added.
*/
.node-tree > ul {
border-left: 2px solid var(--color-border);
padding-left: var(--space-4);
}
.node-tree ul ul {
border-left: 1px solid var(--color-border);
margin-top: var(--space-2);
margin-left: var(--space-2);
padding-left: var(--space-4);
}
.node-tree li {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: var(--space-2);
padding: var(--space-2) 0;
}
.node-tree li > ul {
flex-basis: 100%;
margin-top: var(--space-2);
}
.node-tree li > span {
font-weight: 500;
}
.node-tree li > button {
padding: var(--space-1) var(--space-3);
font-size: var(--font-size-sm);
background-color: var(--color-surface);
border: 1px solid var(--color-border-strong);
border-radius: var(--radius-sm);
}
.node-tree li > button.toggle-children {
padding: var(--space-1) var(--space-2);
background-color: transparent;
border-color: transparent;
color: var(--color-text-muted);
font-size: var(--font-size-base);
}
.node-tree li > button.toggle-children:hover,
.node-tree li > button.toggle-children:focus {
color: var(--color-text);
background-color: var(--color-surface-alt);
}
.node-tree li > button.add-child {
color: var(--color-primary);
border-color: var(--color-primary);
}
.node-tree li > button.add-child:hover,
.node-tree li > button.add-child:focus {
background-color: var(--color-primary);
color: #ffffff;
}
.node-tree li > button.bulk-add-children {
color: var(--color-accent);
border-color: var(--color-accent);
}
.node-tree li > button.bulk-add-children:hover,
.node-tree li > button.bulk-add-children:focus {
background-color: var(--color-accent);
color: #ffffff;
}
.node-tree li > input {
width: auto;
margin-top: 0;
}
.node-tree li > input.bulk-count {
max-width: 6rem;
}

View file

@ -9,9 +9,11 @@ document.addEventListener('DOMContentLoaded', () => {
const texts = await response.json(); const texts = await response.json();
textsList.innerHTML = texts textsList.innerHTML = texts
.map(text => .map(text =>
'<li>' + text.name + '<li class="card cluster cluster-between">' +
' <button class="create-plan" data-text-id="' + '<span class="text-name">' + text.name + '</span>' +
text.id + '">Create plan</button></li>' '<button class="create-plan btn btn-primary"' +
' data-text-id="' + text.id + '">Create plan</button>' +
'</li>'
) )
.join(''); .join('');
} }

View file

@ -8,7 +8,8 @@ document.addEventListener('DOMContentLoaded', () => {
}); });
const texts = await res.json(); const texts = await res.json();
textsList.innerHTML = texts.map(text => textsList.innerHTML = texts.map(text =>
'<li><a href=/admin/texts/' '<li class="card"><a class="card-link"'
+ ' href=/admin/texts/'
+ text.id + text.id
+ '>' + '>'
+ text.name + text.name
@ -26,7 +27,9 @@ document.addEventListener('DOMContentLoaded', () => {
if (res.ok) { if (res.ok) {
const text = await res.json(); const text = await res.json();
const li = document.createElement('li'); const li = document.createElement('li');
li.className = 'card';
const a = document.createElement('a'); const a = document.createElement('a');
a.className = 'card-link';
a.href = '/admin/texts/' + text.id; a.href = '/admin/texts/' + text.id;
a.textContent = text.name; a.textContent = text.name;
li.appendChild(a); li.appendChild(a);

View file

@ -2,6 +2,7 @@ document.addEventListener('DOMContentLoaded', () => {
const scheduledNodesList = document.getElementById( const scheduledNodesList = document.getElementById(
'scheduled-nodes-list' 'scheduled-nodes-list'
); );
const emptyMessage = document.getElementById('scheduled-nodes-empty');
function todayDateString() { function todayDateString() {
const today = new Date(); const today = new Date();
@ -23,10 +24,17 @@ document.addEventListener('DOMContentLoaded', () => {
const scheduledNodes = await response.json(); const scheduledNodes = await response.json();
scheduledNodesList.innerHTML = scheduledNodes scheduledNodesList.innerHTML = scheduledNodes
.map((scheduledNode) => .map((scheduledNode) =>
'<li>' + scheduledNode.planName + ': ' + '<li class="card">' +
scheduledNode.nodeTitle + '</li>' '<span class="muted">' + scheduledNode.planName +
'</span>: ' +
'<span class="node-title">' + scheduledNode.nodeTitle +
'</span>' +
'</li>'
) )
.join(''); .join('');
if (emptyMessage !== null) {
emptyMessage.hidden = scheduledNodes.length > 0;
}
} }
loadScheduledNodes(); loadScheduledNodes();

View file

@ -1,11 +1,29 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Daily Goals - Admin</title> <title>Daily Goals - Admin</title>
<link rel="stylesheet" href="/css/app.css">
</head> </head>
<body> <body>
<button id="logout">Logout</button> <header class="site-header">
<a href="/admin/texts" id="texts">Texts</a> <div class="site-header-inner">
<h1>Admin</h1>
<div class="cluster">
<button id="logout" class="btn btn-danger">Logout</button>
</div>
</div>
</header>
<main class="container stack">
<ul class="list-cards">
<li class="card">
<a id="texts" class="card-link" href="/admin/texts">
Texts
</a>
</li>
</ul>
</main>
<script src="/js/auth.js"></script> <script src="/js/auth.js"></script>
</body> </body>
</html> </html>

View file

@ -1,11 +1,20 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Daily Goals - Forbidden</title> <title>Daily Goals - Forbidden</title>
<link rel="stylesheet" href="/css/app.css">
</head> </head>
<body> <body>
<h1>403 Forbidden</h1> <main class="auth-shell">
<p>You do not have permission to access this page.</p> <div class="auth-card stack text-center">
<a href="/home">Back to Home</a> <h1>403 Forbidden</h1>
<p class="muted">
You do not have permission to access this page.
</p>
<a class="btn btn-primary" href="/home">Back to Home</a>
</div>
</main>
</body> </body>
</html> </html>

View file

@ -1,27 +1,43 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Daily Goals - Home</title> <title>Daily Goals - Home</title>
<link rel="stylesheet" href="/css/app.css">
</head> </head>
<body> <body>
<h1>Home</h1> <header class="site-header">
<button id="logout">Logout</button> <div class="site-header-inner">
<a href="/today">Today's schedule</a> <h1>Home</h1>
<ul id="texts-list"> <div class="cluster">
</ul> <a class="btn btn-secondary" href="/today">
<div id="create-plan-modal" hidden> Today's schedule
<h2>Create plan</h2> </a>
<label>Name <button id="logout" class="btn btn-danger">Logout</button>
<input class="plan-name" type="text" /> </div>
</label> </div>
<label>Start date </header>
<input class="plan-date-start" type="date" /> <main class="container stack-lg">
</label> <ul id="texts-list" class="list-cards"></ul>
<label>End date </main>
<input class="plan-date-end" type="date" /> <div id="create-plan-modal" class="modal" hidden>
</label> <div class="modal-content stack">
<button class="save-plan">Save</button> <h2>Create plan</h2>
<button class="cancel-plan">Cancel</button> <label>Name
<input class="plan-name" type="text" />
</label>
<label>Start date
<input class="plan-date-start" type="date" />
</label>
<label>End date
<input class="plan-date-end" type="date" />
</label>
<div class="cluster cluster-end">
<button class="cancel-plan btn btn-secondary">Cancel</button>
<button class="save-plan btn btn-primary">Save</button>
</div>
</div>
</div> </div>
<script src="/js/auth.js"></script> <script src="/js/auth.js"></script>
<script src="/js/home.js"></script> <script src="/js/home.js"></script>

View file

@ -1,26 +1,37 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Daily Goals - Login</title> <title>Daily Goals - Login</title>
<link rel="stylesheet" href="/css/app.css">
</head> </head>
<body> <body>
<h1>Login</h1> <main class="auth-shell">
<form id="login-form"> <div class="auth-card stack">
<label>Email <h1>Login</h1>
<input type="email" id="email" name="email" required /> <form id="login-form" class="stack">
</label> <label>Email
<label>Password <input type="email" id="email" name="email" required />
<input </label>
type="password" <label>Password
id="password" <input
name="password" type="password"
required id="password"
/> name="password"
</label> required
<button type="submit">Login</button> />
</form> </label>
<p id="login-error" hidden></p> <button type="submit" class="btn btn-primary full-width">
<p><a href="/register">Register</a></p> Login
</button>
</form>
<p id="login-error" class="error" hidden></p>
<p class="muted">
<a href="/register">Need an account? Register</a>
</p>
</div>
</main>
<script src="/js/auth.js"></script> <script src="/js/auth.js"></script>
</body> </body>
</html> </html>

View file

@ -1,27 +1,38 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Daily Goals - Register</title> <title>Daily Goals - Register</title>
<link rel="stylesheet" href="/css/app.css">
</head> </head>
<body> <body>
<h1>Register</h1> <main class="auth-shell">
<form id="register-form"> <div class="auth-card stack">
<label>Email <h1>Register</h1>
<input type="email" id="email" name="email" required /> <form id="register-form" class="stack">
</label> <label>Email
<label>Password (min 8 characters) <input type="email" id="email" name="email" required />
<input </label>
type="password" <label>Password (min 8 characters)
id="password" <input
name="password" type="password"
minlength="8" id="password"
required name="password"
/> minlength="8"
</label> required
<button type="submit">Register</button> />
</form> </label>
<p id="register-error" hidden></p> <button type="submit" class="btn btn-primary full-width">
<p><a href="/login">Already have an account? Login</a></p> Register
</button>
</form>
<p id="register-error" class="error" hidden></p>
<p class="muted">
<a href="/login">Already have an account? Login</a>
</p>
</div>
</main>
<script src="/js/auth.js"></script> <script src="/js/auth.js"></script>
</body> </body>
</html> </html>

View file

@ -1,11 +1,22 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Daily Goals - Text</title> <title>Daily Goals - Text</title>
<link rel="stylesheet" href="/css/app.css">
</head> </head>
<body> <body>
<a href="/admin/texts" id="back">Back to Texts</a> <header class="site-header">
<div id="text-detail"></div> <div class="site-header-inner">
<a class="btn btn-secondary" href="/admin/texts" id="back">
Back to Texts
</a>
</div>
</header>
<main class="container container-wide stack">
<div id="text-detail" class="node-tree stack"></div>
</main>
<script src="/js/text.js"></script> <script src="/js/text.js"></script>
</body> </body>
</html> </html>

View file

@ -1,17 +1,36 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Daily Goals - Texts</title> <title>Daily Goals - Texts</title>
<link rel="stylesheet" href="/css/app.css">
</head> </head>
<body> <body>
<h1>Texts</h1> <header class="site-header">
<a href="/admin" id="back">Back to Admin</a> <div class="site-header-inner">
<ul id="texts-list"> <h1>Texts</h1>
</ul> <div class="cluster">
<form id="texts-form" action="/api/texts" method="POST"> <a class="btn btn-secondary" href="/admin" id="back">
<input id="newTextName" name="name"/> Back to Admin
<button id="submit">submit</button> </a>
</form> </div>
</div>
</header>
<main class="container stack-lg">
<ul id="texts-list" class="list-cards"></ul>
<form id="texts-form" action="/api/texts" method="POST"
class="card stack">
<label>New text name
<input id="newTextName" name="name" type="text" />
</label>
<div class="cluster cluster-end">
<button id="submit" class="btn btn-primary" type="submit">
Add text
</button>
</div>
</form>
</main>
<script src="/js/texts.js"></script> <script src="/js/texts.js"></script>
</body> </body>
</html> </html>

View file

@ -1,12 +1,26 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Daily Goals - Today</title> <title>Daily Goals - Today</title>
<link rel="stylesheet" href="/css/app.css">
</head> </head>
<body> <body>
<h1>Today</h1> <header class="site-header">
<ul id="scheduled-nodes-list"> <div class="site-header-inner">
</ul> <h1>Today</h1>
<div class="cluster">
<a class="btn btn-secondary" href="/home">Home</a>
</div>
</div>
</header>
<main class="container stack">
<ul id="scheduled-nodes-list" class="list-cards"></ul>
<p id="scheduled-nodes-empty" class="muted" hidden>
Nothing scheduled for today.
</p>
</main>
<script src="/js/auth.js"></script> <script src="/js/auth.js"></script>
<script src="/js/today.js"></script> <script src="/js/today.js"></script>
</body> </body>