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();
textsList.innerHTML = texts
.map(text =>
'<li>' + text.name +
' <button class="create-plan" data-text-id="' +
text.id + '">Create plan</button></li>'
'<li class="card cluster cluster-between">' +
'<span class="text-name">' + text.name + '</span>' +
'<button class="create-plan btn btn-primary"' +
' data-text-id="' + text.id + '">Create plan</button>' +
'</li>'
)
.join('');
}

View file

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

View file

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

View file

@ -1,11 +1,29 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Daily Goals - Admin</title>
<link rel="stylesheet" href="/css/app.css">
</head>
<body>
<button id="logout">Logout</button>
<a href="/admin/texts" id="texts">Texts</a>
<header class="site-header">
<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>
</body>
</html>

View file

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

View file

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

View file

@ -1,11 +1,16 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Daily Goals - Login</title>
<link rel="stylesheet" href="/css/app.css">
</head>
<body>
<main class="auth-shell">
<div class="auth-card stack">
<h1>Login</h1>
<form id="login-form">
<form id="login-form" class="stack">
<label>Email
<input type="email" id="email" name="email" required />
</label>
@ -17,10 +22,16 @@
required
/>
</label>
<button type="submit">Login</button>
<button type="submit" class="btn btn-primary full-width">
Login
</button>
</form>
<p id="login-error" hidden></p>
<p><a href="/register">Register</a></p>
<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>
</body>
</html>

View file

@ -1,11 +1,16 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Daily Goals - Register</title>
<link rel="stylesheet" href="/css/app.css">
</head>
<body>
<main class="auth-shell">
<div class="auth-card stack">
<h1>Register</h1>
<form id="register-form">
<form id="register-form" class="stack">
<label>Email
<input type="email" id="email" name="email" required />
</label>
@ -18,10 +23,16 @@
required
/>
</label>
<button type="submit">Register</button>
<button type="submit" class="btn btn-primary full-width">
Register
</button>
</form>
<p id="register-error" hidden></p>
<p><a href="/login">Already have an account? Login</a></p>
<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>
</body>
</html>

View file

@ -1,11 +1,22 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Daily Goals - Text</title>
<link rel="stylesheet" href="/css/app.css">
</head>
<body>
<a href="/admin/texts" id="back">Back to Texts</a>
<div id="text-detail"></div>
<header class="site-header">
<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>
</body>
</html>

View file

@ -1,17 +1,36 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Daily Goals - Texts</title>
<link rel="stylesheet" href="/css/app.css">
</head>
<body>
<header class="site-header">
<div class="site-header-inner">
<h1>Texts</h1>
<a href="/admin" id="back">Back to Admin</a>
<ul id="texts-list">
</ul>
<form id="texts-form" action="/api/texts" method="POST">
<input id="newTextName" name="name"/>
<button id="submit">submit</button>
<div class="cluster">
<a class="btn btn-secondary" href="/admin" id="back">
Back to Admin
</a>
</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>
</body>
</html>

View file

@ -1,12 +1,26 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Daily Goals - Today</title>
<link rel="stylesheet" href="/css/app.css">
</head>
<body>
<header class="site-header">
<div class="site-header-inner">
<h1>Today</h1>
<ul id="scheduled-nodes-list">
</ul>
<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/today.js"></script>
</body>