Merge commit 'b3b4f77'
# Conflicts: # themes/intotheeast/templates/partials/trip-feed-col.html.twig # themes/intotheeast/templates/trip.html.twig
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -269,6 +269,103 @@ body::after {
|
||||
gap: var(--space-1);
|
||||
}
|
||||
|
||||
/* ── Owner card controls (U4): Draft badge + Edit/Delete ─────────────────── */
|
||||
.journal-post-titlerow {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap; /* phone-first: actions wrap below a long title */
|
||||
gap: var(--space-2) var(--space-3);
|
||||
margin-bottom: var(--space-2);
|
||||
}
|
||||
.journal-post-titlerow .journal-post-title { margin-bottom: 0; }
|
||||
|
||||
.journal-draft-badge {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
margin-left: var(--space-2);
|
||||
font-family: var(--font-sans);
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.09em;
|
||||
text-transform: uppercase;
|
||||
color: #E0A458; /* warm amber — draft/unpublished */
|
||||
border: 1px solid #E0A458;
|
||||
border-radius: var(--radius-sm);
|
||||
padding: 0.1em 0.5em;
|
||||
line-height: 1.5;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.journal-post-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
flex-wrap: wrap;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.entry-action {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 44px; /* D8 — phone-first tap target */
|
||||
min-width: 44px;
|
||||
padding: 0 var(--space-3);
|
||||
font-family: var(--font-sans);
|
||||
font-size: var(--text-xs);
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.04em;
|
||||
border-radius: var(--radius-sm);
|
||||
border: 1px solid var(--color-border);
|
||||
background: transparent;
|
||||
color: var(--color-ink-2);
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
transition: background .15s, color .15s, border-color .15s;
|
||||
}
|
||||
.entry-action:hover,
|
||||
.entry-action:focus-visible {
|
||||
border-color: var(--color-accent);
|
||||
color: var(--color-accent);
|
||||
outline: none;
|
||||
}
|
||||
/* [hidden] must win over the inline-flex display above (used to swap
|
||||
Delete <-> Cancel/Confirm from feed-actions.js). */
|
||||
.entry-action[hidden] { display: none; }
|
||||
.entry-action--confirm {
|
||||
color: #E5786A; /* soft red — destructive confirm */
|
||||
border-color: #E5786A;
|
||||
}
|
||||
.entry-action--confirm:hover,
|
||||
.entry-action--confirm:focus-visible {
|
||||
background: #E5786A;
|
||||
color: var(--color-ink-inverse);
|
||||
}
|
||||
.entry-delete-confirm {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
.entry-delete-confirm[hidden] { display: none; }
|
||||
.entry-delete-msg {
|
||||
font-size: var(--text-xs);
|
||||
color: #E5786A;
|
||||
}
|
||||
.entry-delete-msg:empty { display: none; }
|
||||
|
||||
/* Visually-hidden but screen-reader-available (feed-actions live region). */
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.journal-photo-wrap {
|
||||
position: relative;
|
||||
margin-bottom: var(--space-5);
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
(()=>{function m(){var e=document.getElementById("feed-actions-live");return e||(e=document.createElement("div"),e.id="feed-actions-live",e.setAttribute("aria-live","polite"),e.setAttribute("role","status"),e.className="sr-only",document.body.appendChild(e)),e}function s(e){m().textContent=e}function v(e){var n=String(e||"").replace(/\/+$/,"").split("/");return n[n.length-1]||""}function u(e,n){var i=e.querySelector("[data-delete-start]"),r=e.querySelector(".entry-delete-confirm");if(i&&(i.hidden=n),r&&(r.hidden=!n),!n){var t=e.querySelector(".entry-delete-msg");t&&(t.textContent="")}}function g(e){var n=e.getAttribute("data-entry-route"),i=v(n),r=e.querySelector("[data-delete-cancel]"),t=e.querySelector("[data-delete-confirm]"),a=e.querySelector(".entry-delete-msg"),l=e.closest(".journal-post");t&&(t.disabled=!0,t.textContent="Deleting\u2026"),r&&(r.disabled=!0),a&&(a.textContent=""),s("Deleting entry\u2026"),fetch("/api/v1/entry/"+encodeURIComponent(i),{method:"DELETE",credentials:"include",headers:{Accept:"application/json"}}).then(function(d){if(!(d.status===204||d.ok))throw new Error("HTTP "+d.status);for(var o=l?l.nextElementSibling:null;o&&!o.classList.contains("journal-post");)o=o.nextElementSibling;var c=o||(l&&l.previousElementSibling&&l.previousElementSibling.classList&&l.previousElementSibling.classList.contains("journal-post")?l.previousElementSibling:null)||document.querySelector(".home-trip-name, .home-feed-col h1, .feed");if(l&&l.remove(),c){c.hasAttribute("tabindex")||c.setAttribute("tabindex","-1");try{c.focus({preventScroll:!1})}catch{}}s("Entry deleted.")}).catch(function(){t&&(t.disabled=!1,t.textContent="Confirm delete"),r&&(r.disabled=!1),a&&(a.textContent="Could not delete \u2014 please try again."),s("Delete failed.")})}function f(){document.addEventListener("click",function(e){var n=e.target.closest?e.target.closest("[data-delete-start]"):null,i=e.target.closest?e.target.closest("[data-delete-cancel]"):null,r=e.target.closest?e.target.closest("[data-delete-confirm]"):null;if(!(!n&&!i&&!r)){var t=(n||i||r).closest(".journal-post-actions");if(t){if(e.preventDefault(),n){u(t,!0);return}if(i){u(t,!1);return}if(r){var a=t.querySelector("[data-delete-confirm]");if(a&&a.disabled)return;g(t)}}}})}document.readyState==="loading"?document.addEventListener("DOMContentLoaded",f):f();})();
|
||||
@@ -0,0 +1 @@
|
||||
var g=Object.create;var f=Object.defineProperty;var h=Object.getOwnPropertyDescriptor;var i=Object.getOwnPropertyNames;var j=Object.getPrototypeOf,k=Object.prototype.hasOwnProperty;var m=(a=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(a,{get:(b,c)=>(typeof require<"u"?require:b)[c]}):a)(function(a){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+a+'" is not supported')});var n=(a,b)=>()=>(b||a((b={exports:{}}).exports,b),b.exports);var l=(a,b,c,e)=>{if(b&&typeof b=="object"||typeof b=="function")for(let d of i(b))!k.call(a,d)&&d!==c&&f(a,d,{get:()=>b[d],enumerable:!(e=h(b,d))||e.enumerable});return a};var o=(a,b,c)=>(c=a!=null?g(j(a)):{},l(b||!a||!a.__esModule?f(c,"default",{value:a,enumerable:!0}):c,a));export{m as a,n as b,o as c};
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,120 @@
|
||||
/*
|
||||
* feed-actions.js (U6) — owner delete wiring for journal feed cards.
|
||||
*
|
||||
* Loaded only on the trip / home active-trip feed for the owner. The Edit link is
|
||||
* a plain navigation (no JS). Delete is a two-step inline confirm (no browser
|
||||
* dialog): Delete → Cancel / Confirm delete → DELETE /api/v1/entry/<slug>
|
||||
* (session cookie), then the card is removed and focus moves to the next card.
|
||||
*
|
||||
* Markup (rendered by partials/entry-journal.html.twig):
|
||||
* article.journal-post[data-entry-route]
|
||||
* .journal-post-actions[data-entry-route]
|
||||
* button[data-delete-start] (Delete)
|
||||
* .entry-delete-confirm[hidden]
|
||||
* button[data-delete-cancel] (Cancel)
|
||||
* button[data-delete-confirm] (Confirm delete)
|
||||
* .entry-delete-msg[aria-live] (inline error slot)
|
||||
*/
|
||||
|
||||
// One page-level polite live region for cross-card announcements (D4) — the
|
||||
// per-card .entry-delete-msg vanishes with the card it belongs to.
|
||||
function liveRegion() {
|
||||
var el = document.getElementById('feed-actions-live');
|
||||
if (!el) {
|
||||
el = document.createElement('div');
|
||||
el.id = 'feed-actions-live';
|
||||
el.setAttribute('aria-live', 'polite');
|
||||
el.setAttribute('role', 'status');
|
||||
el.className = 'sr-only';
|
||||
document.body.appendChild(el);
|
||||
}
|
||||
return el;
|
||||
}
|
||||
function announce(msg) { liveRegion().textContent = msg; }
|
||||
|
||||
function slugFromRoute(route) {
|
||||
var parts = String(route || '').replace(/\/+$/, '').split('/');
|
||||
return parts[parts.length - 1] || '';
|
||||
}
|
||||
|
||||
function showConfirm(actions, show) {
|
||||
var del = actions.querySelector('[data-delete-start]');
|
||||
var confirm = actions.querySelector('.entry-delete-confirm');
|
||||
if (del) del.hidden = show;
|
||||
if (confirm) confirm.hidden = !show;
|
||||
if (!show) {
|
||||
var msg = actions.querySelector('.entry-delete-msg');
|
||||
if (msg) msg.textContent = '';
|
||||
}
|
||||
}
|
||||
|
||||
function performDelete(actions) {
|
||||
var route = actions.getAttribute('data-entry-route');
|
||||
var slug = slugFromRoute(route);
|
||||
var cancelBtn = actions.querySelector('[data-delete-cancel]');
|
||||
var confirmBtn = actions.querySelector('[data-delete-confirm]');
|
||||
var msg = actions.querySelector('.entry-delete-msg');
|
||||
var card = actions.closest('.journal-post');
|
||||
|
||||
// D2: lock both buttons immediately so a mobile double-tap can't fire two
|
||||
// DELETEs (the second would 404 on an already-removed folder).
|
||||
if (confirmBtn) { confirmBtn.disabled = true; confirmBtn.textContent = 'Deleting…'; }
|
||||
if (cancelBtn) cancelBtn.disabled = true;
|
||||
if (msg) msg.textContent = '';
|
||||
announce('Deleting entry…');
|
||||
|
||||
fetch('/api/v1/entry/' + encodeURIComponent(slug), {
|
||||
method: 'DELETE',
|
||||
credentials: 'include',
|
||||
headers: { Accept: 'application/json' }
|
||||
}).then(function (r) {
|
||||
if (!(r.status === 204 || r.ok)) throw new Error('HTTP ' + r.status);
|
||||
|
||||
// D4: move focus to the next journal card (or the feed heading) BEFORE
|
||||
// removing this one, then announce.
|
||||
var next = card ? card.nextElementSibling : null;
|
||||
while (next && !next.classList.contains('journal-post')) next = next.nextElementSibling;
|
||||
var focusTarget = next
|
||||
|| (card && card.previousElementSibling && card.previousElementSibling.classList && card.previousElementSibling.classList.contains('journal-post') ? card.previousElementSibling : null)
|
||||
|| document.querySelector('.home-trip-name, .home-feed-col h1, .feed');
|
||||
if (card) card.remove();
|
||||
if (focusTarget) {
|
||||
if (!focusTarget.hasAttribute('tabindex')) focusTarget.setAttribute('tabindex', '-1');
|
||||
try { focusTarget.focus({ preventScroll: false }); } catch (e) { /* older browsers */ }
|
||||
}
|
||||
announce('Entry deleted.');
|
||||
}).catch(function () {
|
||||
// D7: restore the control + inline error, constrained to the card.
|
||||
if (confirmBtn) { confirmBtn.disabled = false; confirmBtn.textContent = 'Confirm delete'; }
|
||||
if (cancelBtn) cancelBtn.disabled = false;
|
||||
if (msg) msg.textContent = 'Could not delete — please try again.';
|
||||
announce('Delete failed.');
|
||||
});
|
||||
}
|
||||
|
||||
function initFeedActions() {
|
||||
document.addEventListener('click', function (e) {
|
||||
var start = e.target.closest ? e.target.closest('[data-delete-start]') : null;
|
||||
var cancel = e.target.closest ? e.target.closest('[data-delete-cancel]') : null;
|
||||
var confirm = e.target.closest ? e.target.closest('[data-delete-confirm]') : null;
|
||||
if (!start && !cancel && !confirm) return;
|
||||
|
||||
var actions = (start || cancel || confirm).closest('.journal-post-actions');
|
||||
if (!actions) return;
|
||||
e.preventDefault();
|
||||
|
||||
if (start) { showConfirm(actions, true); return; }
|
||||
if (cancel) { showConfirm(actions, false); return; }
|
||||
if (confirm) {
|
||||
var btn = actions.querySelector('[data-delete-confirm]');
|
||||
if (btn && btn.disabled) return; // already in flight (D2)
|
||||
performDelete(actions);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initFeedActions);
|
||||
} else {
|
||||
initFeedActions();
|
||||
}
|
||||
@@ -0,0 +1,521 @@
|
||||
/*
|
||||
* /post form bundle CSS. U3 scope: make the EasyMDE editor usable without the
|
||||
* FontAwesome dependency (toolbar glyphs supplied here). Field Notes theming,
|
||||
* mobile layout, and the "More options" disclosure land in U5.
|
||||
*/
|
||||
|
||||
/* Edit-mode prefill-failure banner (U5, D7): shown between the heading and the
|
||||
first field when the entry can't be loaded for editing. */
|
||||
.post-edit-error {
|
||||
margin: 1rem 0;
|
||||
padding: 0.75rem 1rem;
|
||||
border: 1px solid #E5786A;
|
||||
border-radius: 8px;
|
||||
background: rgba(229, 120, 106, 0.12);
|
||||
color: #E5786A;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
/* EasyMDE toolbar glyphs — replace the FontAwesome icons EasyMDE expects. */
|
||||
.editor-toolbar .mde-btn::before {
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-family: inherit;
|
||||
}
|
||||
.editor-toolbar .mde-bold::before { content: 'B'; font-weight: 700; }
|
||||
.editor-toolbar .mde-italic::before { content: 'I'; font-style: italic; }
|
||||
.editor-toolbar .mde-ul::before { content: '\2022\2002\2014'; } /* • — */
|
||||
.editor-toolbar .mde-link::before { content: '\1F517'; } /* 🔗 */
|
||||
.editor-toolbar .mde-preview::before { content: '\1F441'; } /* 👁 */
|
||||
|
||||
.EasyMDEContainer .CodeMirror {
|
||||
min-height: 180px;
|
||||
}
|
||||
|
||||
/* ── Photo HEIC-conversion feedback (U4) ────────────────────
|
||||
* FilePond renders the thumbnails/progress; these are the pre-FilePond
|
||||
* "converting…" status line and the draft photos-reselect hint (U6).
|
||||
*/
|
||||
.photo-convert-status:empty { display: none; }
|
||||
.photo-convert-status {
|
||||
font-size: var(--text-sm);
|
||||
margin-top: var(--space-2);
|
||||
}
|
||||
.photo-reauth-hint {
|
||||
display: none;
|
||||
font-size: var(--text-sm);
|
||||
color: var(--color-ink-muted);
|
||||
margin-top: var(--space-2);
|
||||
}
|
||||
.photo-reauth-hint.is-shown { display: block; }
|
||||
|
||||
/* ── Field Notes styling for the new controls (U5, R11/R12) ── */
|
||||
|
||||
/* Select fields (weather condition, transport mode) match the text inputs. */
|
||||
.post-form-wrap select {
|
||||
width: 100%;
|
||||
font-family: var(--font-ui);
|
||||
font-size: var(--text-base);
|
||||
padding: 0.875rem 1rem;
|
||||
min-height: 44px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--color-canvas);
|
||||
color: var(--color-ink);
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'%3E%3Cpath fill='%2390887E' d='M1 1l5 5 5-5'/%3E%3C/svg%3E");
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 1rem center;
|
||||
padding-right: 2.5rem;
|
||||
}
|
||||
.post-form-wrap select:focus {
|
||||
outline: 2px solid var(--color-accent);
|
||||
outline-offset: 1px;
|
||||
border-color: var(--color-accent);
|
||||
}
|
||||
.post-form-wrap input[type="number"] {
|
||||
width: 100%;
|
||||
font-family: var(--font-ui);
|
||||
font-size: var(--text-base);
|
||||
padding: 0.875rem 1rem;
|
||||
min-height: 44px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--color-canvas);
|
||||
color: var(--color-ink);
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
}
|
||||
.post-form-wrap input[type="number"]:focus {
|
||||
outline: 2px solid var(--color-accent);
|
||||
outline-offset: 1px;
|
||||
border-color: var(--color-accent);
|
||||
}
|
||||
|
||||
/* "More options" disclosure */
|
||||
.more-options {
|
||||
margin-bottom: var(--space-5);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--color-canvas);
|
||||
}
|
||||
.more-options__summary {
|
||||
cursor: pointer;
|
||||
padding: 0.875rem 1rem;
|
||||
min-height: 44px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-family: var(--font-ui);
|
||||
font-size: var(--text-sm);
|
||||
font-weight: 600;
|
||||
color: var(--color-ink);
|
||||
list-style: none;
|
||||
user-select: none;
|
||||
}
|
||||
.more-options__summary::-webkit-details-marker { display: none; }
|
||||
.more-options__summary::before {
|
||||
content: '▸';
|
||||
margin-right: var(--space-2);
|
||||
color: var(--color-ink-muted);
|
||||
transition: transform 0.15s;
|
||||
}
|
||||
.more-options[open] .more-options__summary::before { transform: rotate(90deg); }
|
||||
.more-options[open] .more-options__summary { border-bottom: 1px solid var(--color-border); }
|
||||
.more-options > .form-field { padding: 0 1rem; }
|
||||
.more-options > .form-field:first-of-type { padding-top: var(--space-4); }
|
||||
.more-options > .form-field:last-child { padding-bottom: var(--space-4); margin-bottom: 0; }
|
||||
|
||||
/* Button loading spinner (Get Location / Get Weather) */
|
||||
.btn-action.is-loading { position: relative; color: transparent; pointer-events: none; }
|
||||
.btn-action.is-loading::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin: -8px 0 0 -8px;
|
||||
border: 2px solid var(--color-ink-muted);
|
||||
border-top-color: var(--color-accent);
|
||||
border-radius: 50%;
|
||||
animation: post-spin 0.7s linear infinite;
|
||||
}
|
||||
.btn-action[disabled] { opacity: 0.5; cursor: not-allowed; }
|
||||
@keyframes post-spin { to { transform: rotate(360deg); } }
|
||||
|
||||
/* Grav renders form status as <div class="notices success|error green|red">.
|
||||
The theme doesn't style .notices, so on the dark background it was nearly
|
||||
invisible — style it prominently here. */
|
||||
.post-form-wrap .notices {
|
||||
padding: 1rem 1.1rem;
|
||||
border-radius: var(--radius-md);
|
||||
font-size: var(--text-base);
|
||||
margin: 0 0 var(--space-5);
|
||||
background: var(--color-canvas);
|
||||
color: var(--color-ink);
|
||||
border-left: 4px solid var(--color-accent);
|
||||
}
|
||||
.post-form-wrap .notices p { margin: 0; }
|
||||
.post-form-wrap .notices.error,
|
||||
.post-form-wrap .notices.red { border-left-color: var(--color-error); }
|
||||
.post-form-wrap .notices.success,
|
||||
.post-form-wrap .notices.green { border-left-color: var(--color-accent); }
|
||||
|
||||
/* Post-success confirmation CTA (injected by post-form.js). */
|
||||
.post-success {
|
||||
margin: 0 0 var(--space-5);
|
||||
padding: 1.1rem;
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--color-accent-light);
|
||||
border: 1px solid var(--color-accent);
|
||||
}
|
||||
.post-success__title {
|
||||
font-family: var(--font-ui);
|
||||
font-size: var(--text-md);
|
||||
font-weight: 600;
|
||||
color: var(--color-ink);
|
||||
margin: 0 0 var(--space-3);
|
||||
}
|
||||
.post-success__actions { display: flex; flex-wrap: wrap; gap: var(--space-3); }
|
||||
.post-success__view {
|
||||
flex: 1;
|
||||
min-width: 140px;
|
||||
text-align: center;
|
||||
padding: 0.8rem 1rem;
|
||||
min-height: 44px;
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--color-accent);
|
||||
color: var(--color-accent-on);
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
}
|
||||
.post-success__again {
|
||||
flex: 1;
|
||||
min-width: 140px;
|
||||
text-align: center;
|
||||
padding: 0.8rem 1rem;
|
||||
min-height: 44px;
|
||||
border-radius: var(--radius-md);
|
||||
background: transparent;
|
||||
color: var(--color-ink);
|
||||
border: 1px solid var(--color-border);
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/* Hide FilePond's "Powered by PQINA" credit. */
|
||||
.filepond--credits { display: none !important; }
|
||||
|
||||
/* Field Notes dark theme for the FilePond widget — the default is a light/cream
|
||||
panel that clashes with the site's warm near-black palette. Repaint the drop
|
||||
zone, thumbnails and actions with the design tokens. */
|
||||
.filepond--root { font-family: var(--font-ui); font-size: var(--text-base); }
|
||||
.filepond--panel-root {
|
||||
background-color: var(--color-canvas);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
.filepond--drop-label,
|
||||
.filepond--drop-label label { color: var(--color-ink-muted); }
|
||||
.filepond--label-action {
|
||||
color: var(--color-accent);
|
||||
text-decoration-color: var(--color-accent);
|
||||
}
|
||||
.filepond--item-panel {
|
||||
background-color: var(--color-surface-raised);
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
.filepond--drip-blob { background-color: var(--color-accent); }
|
||||
.filepond--file { color: var(--color-ink); }
|
||||
.filepond--file-action-button {
|
||||
color: var(--color-ink);
|
||||
background-color: rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
.filepond--file-action-button:hover { background-color: rgba(0, 0, 0, 0.65); }
|
||||
|
||||
/* Issue #2: FilePond's completed thumbnail carries a murky gradient tint + the
|
||||
filename overlay, which reads as "something's wrong". Strip those overlays
|
||||
and show a single clean green ✓ badge so a finished upload is unambiguous.
|
||||
The remove (×) action button is left untouched so photos stay removable. */
|
||||
.filepond--image-preview-overlay { display: none !important; }
|
||||
.filepond--item[data-filepond-item-state="processing-complete"] .filepond--file-info,
|
||||
.filepond--item[data-filepond-item-state="processing-complete"] .filepond--file-status {
|
||||
opacity: 0;
|
||||
}
|
||||
.filepond--item[data-filepond-item-state="processing-complete"]::after {
|
||||
content: '✓';
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
z-index: 5;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
line-height: 24px;
|
||||
text-align: center;
|
||||
border-radius: 50%;
|
||||
background: var(--color-accent);
|
||||
color: #fff;
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.35);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* ── Photo thumbnail grid (edit + create) ─────────────────────────────────
|
||||
Render FilePond items as a compact square-thumbnail grid so existing photos
|
||||
(loaded on edit) and new uploads are visible and reorderable without the
|
||||
full-width, overflowing vertical list the default produced. Setting a fixed
|
||||
item width is FilePond's supported way to switch its layout engine to a grid. */
|
||||
.filepond--list.filepond--list {
|
||||
/* FilePond positions items absolutely; the wrapper needs a little inset so
|
||||
the 3-up grid doesn't butt against the drop-zone edge. */
|
||||
margin: 0.25rem;
|
||||
}
|
||||
.filepond--item {
|
||||
width: calc(33.333% - 0.5rem);
|
||||
}
|
||||
/* Square thumbnails regardless of source aspect ratio. */
|
||||
.filepond--item .filepond--panel-root,
|
||||
.filepond--item .filepond--image-preview-wrapper { border-radius: var(--radius-sm); }
|
||||
|
||||
/* Native image dragging hijacked FilePond's reorder: dragging a thumbnail
|
||||
started a browser ghost-image / page scroll instead of moving the item.
|
||||
Disable user-drag on the preview so FilePond owns the gesture. */
|
||||
.filepond--image-preview,
|
||||
.filepond--image-preview-wrapper,
|
||||
.filepond--image-clip,
|
||||
.filepond--image-preview canvas,
|
||||
.filepond--image-bitmap,
|
||||
.filepond--file img {
|
||||
-webkit-user-drag: none;
|
||||
-khtml-user-drag: none;
|
||||
user-drag: none;
|
||||
}
|
||||
|
||||
/* Existing photos loaded for editing sit in FilePond's 'idle' state (they are
|
||||
already on the server, never uploaded). Give them the same clean look as a
|
||||
completed upload — hide the filename/size overlay so the grid reads as photos,
|
||||
not a list of filenames. */
|
||||
.filepond--item[data-filepond-item-state="idle"] .filepond--file-info,
|
||||
.filepond--item[data-filepond-item-state="idle"] .filepond--file-status {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* Issue #3: photo section auto-collapses to a summary bar after upload — same
|
||||
<details> affordance as "More options" above. */
|
||||
.photos-collapse {
|
||||
margin-bottom: var(--space-5);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--color-canvas);
|
||||
}
|
||||
.photos-collapse__summary {
|
||||
cursor: pointer;
|
||||
padding: 0.875rem 1rem;
|
||||
min-height: 44px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-family: var(--font-ui);
|
||||
font-size: var(--text-sm);
|
||||
font-weight: 600;
|
||||
color: var(--color-ink);
|
||||
list-style: none;
|
||||
user-select: none;
|
||||
}
|
||||
.photos-collapse__summary::-webkit-details-marker { display: none; }
|
||||
.photos-collapse__summary::before {
|
||||
content: '▸';
|
||||
margin-right: var(--space-2);
|
||||
color: var(--color-ink-muted);
|
||||
transition: transform 0.15s;
|
||||
}
|
||||
.photos-collapse[open] .photos-collapse__summary::before { transform: rotate(90deg); }
|
||||
.photos-collapse[open] .photos-collapse__summary { border-bottom: 1px solid var(--color-border); }
|
||||
.photos-collapse > :not(summary) { padding-left: 1rem; padding-right: 1rem; }
|
||||
.photos-collapse[open] .filepond--root,
|
||||
.photos-collapse[open] .filepond-root { margin: var(--space-4) 0; }
|
||||
.photos-collapse[open] > .photo-convert-status:last-child,
|
||||
.photos-collapse[open] > .photo-reauth-hint:last-child { padding-bottom: var(--space-4); }
|
||||
|
||||
/* ── EasyMDE — Field Notes dark theme (U5) ─────────────────── */
|
||||
.EasyMDEContainer .CodeMirror {
|
||||
background: var(--color-canvas);
|
||||
color: var(--color-ink);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 0 0 var(--radius-md) var(--radius-md);
|
||||
font-family: var(--font-ui);
|
||||
font-size: var(--text-base);
|
||||
line-height: var(--leading-normal);
|
||||
padding: var(--space-1);
|
||||
}
|
||||
.EasyMDEContainer .CodeMirror-cursor { border-color: var(--color-ink); }
|
||||
.EasyMDEContainer .CodeMirror-selected { background: var(--color-accent-light) !important; }
|
||||
.EasyMDEContainer .editor-toolbar {
|
||||
background: var(--color-surface-raised);
|
||||
border: 1px solid var(--color-border);
|
||||
border-bottom: none;
|
||||
border-radius: var(--radius-md) var(--radius-md) 0 0;
|
||||
opacity: 1;
|
||||
}
|
||||
.EasyMDEContainer .editor-toolbar button {
|
||||
color: var(--color-ink) !important;
|
||||
min-width: 34px;
|
||||
height: 34px;
|
||||
}
|
||||
.EasyMDEContainer .editor-toolbar button:hover,
|
||||
.EasyMDEContainer .editor-toolbar button.active {
|
||||
background: var(--color-accent-light);
|
||||
border-color: var(--color-border);
|
||||
}
|
||||
.EasyMDEContainer .editor-toolbar i.separator { border-color: var(--color-border); }
|
||||
.EasyMDEContainer .editor-preview,
|
||||
.EasyMDEContainer .editor-preview-side {
|
||||
background: var(--color-canvas);
|
||||
color: var(--color-ink);
|
||||
}
|
||||
.EasyMDEContainer .editor-preview a { color: var(--color-accent); }
|
||||
|
||||
/* ── Photo editor (edit mode only) ────────────────────────────────────────
|
||||
Own thumbnail grid that talks straight to the media API (add/delete/reorder
|
||||
live), replacing the FilePond photo path when editing. */
|
||||
.photo-editor {
|
||||
margin-bottom: var(--space-5);
|
||||
}
|
||||
.photo-editor__head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--space-3);
|
||||
margin-bottom: var(--space-2);
|
||||
}
|
||||
.photo-editor__label {
|
||||
font-family: var(--font-ui);
|
||||
font-size: var(--text-base);
|
||||
font-weight: 600;
|
||||
color: var(--color-ink);
|
||||
}
|
||||
.photo-editor__add {
|
||||
font-family: var(--font-ui);
|
||||
font-size: var(--text-sm);
|
||||
background: var(--color-accent);
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: var(--radius-sm);
|
||||
padding: var(--space-2) var(--space-4);
|
||||
cursor: pointer;
|
||||
}
|
||||
.photo-editor__add:disabled { opacity: 0.5; cursor: default; }
|
||||
|
||||
.photo-editor__status {
|
||||
font-size: var(--text-sm);
|
||||
color: var(--color-ink-muted);
|
||||
min-height: 1.2em;
|
||||
margin: 0 0 var(--space-2);
|
||||
}
|
||||
.photo-editor__status.error { color: var(--color-error); }
|
||||
|
||||
.photo-editor__grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: var(--space-2);
|
||||
}
|
||||
@media (min-width: 600px) {
|
||||
.photo-editor__grid { grid-template-columns: repeat(4, 1fr); }
|
||||
}
|
||||
.photo-editor__loading,
|
||||
.photo-editor__empty {
|
||||
grid-column: 1 / -1;
|
||||
color: var(--color-ink-muted);
|
||||
font-size: var(--text-sm);
|
||||
padding: var(--space-4) 0;
|
||||
}
|
||||
|
||||
.photo-editor__cell {
|
||||
position: relative;
|
||||
aspect-ratio: 1 / 1;
|
||||
border-radius: var(--radius-sm);
|
||||
overflow: hidden;
|
||||
background: var(--color-surface-raised);
|
||||
cursor: grab;
|
||||
touch-action: none; /* let SortableJS own the touch-drag gesture */
|
||||
}
|
||||
.photo-editor__cell:active { cursor: grabbing; }
|
||||
.photo-editor__img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
-webkit-user-drag: none;
|
||||
user-select: none;
|
||||
pointer-events: none; /* the cell, not the image, is the drag handle */
|
||||
}
|
||||
/* First thumbnail is the feed cover. */
|
||||
.photo-editor__cell:first-child::after {
|
||||
content: 'Cover';
|
||||
position: absolute;
|
||||
left: var(--space-1);
|
||||
bottom: var(--space-1);
|
||||
font-family: var(--font-ui);
|
||||
font-size: var(--text-xs, 0.7rem);
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
background: var(--color-accent);
|
||||
padding: 1px 6px;
|
||||
border-radius: var(--radius-sm);
|
||||
pointer-events: none;
|
||||
}
|
||||
.photo-editor__del {
|
||||
position: absolute;
|
||||
top: var(--space-1);
|
||||
right: var(--space-1);
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
line-height: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
background: rgba(0, 0, 0, 0.55);
|
||||
color: #fff;
|
||||
font-size: 0.85rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
.photo-editor__del:hover { background: rgba(0, 0, 0, 0.75); }
|
||||
.photo-editor__del:disabled { opacity: 0.4; cursor: default; }
|
||||
|
||||
/* Inline delete confirm — covers the cell (option a: live delete, deliberate step). */
|
||||
.photo-editor__confirm {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: var(--space-1);
|
||||
padding: var(--space-2);
|
||||
background: rgba(0, 0, 0, 0.72);
|
||||
text-align: center;
|
||||
}
|
||||
.photo-editor__confirm-q {
|
||||
color: #fff;
|
||||
font-family: var(--font-ui);
|
||||
font-size: var(--text-sm);
|
||||
}
|
||||
.photo-editor__confirm button {
|
||||
font-family: var(--font-ui);
|
||||
font-size: var(--text-xs, 0.72rem);
|
||||
border: none;
|
||||
border-radius: var(--radius-sm);
|
||||
padding: 3px 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.photo-editor__confirm-yes { background: var(--color-error); color: #fff; }
|
||||
.photo-editor__confirm-no { background: #fff; color: var(--color-ink); }
|
||||
.photo-editor__confirm button:disabled { opacity: 0.5; cursor: default; }
|
||||
|
||||
/* SortableJS drag feedback. */
|
||||
.photo-editor__cell.sortable-ghost { opacity: 0.4; }
|
||||
.photo-editor__cell.sortable-chosen { outline: 2px solid var(--color-accent); }
|
||||
File diff suppressed because it is too large
Load Diff
Generated
+92
-1
@@ -8,9 +8,12 @@
|
||||
"@fontsource-variable/dm-sans": "latest",
|
||||
"@fontsource/dm-serif-display": "latest",
|
||||
"@mapbox/togeojson": "^0.16.2",
|
||||
"easymde": "^2",
|
||||
"heic-to": "^1",
|
||||
"maplibre-gl": "^4",
|
||||
"photoswipe": "^5",
|
||||
"scrollama": "^3"
|
||||
"scrollama": "^3",
|
||||
"sortablejs": "^1.15.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"esbuild": "^0.21",
|
||||
@@ -523,6 +526,21 @@
|
||||
"integrity": "sha512-RKJ22hX8mHe3Y6wH/N3wCM6BWtjaxIyyUIkpHOvfFnxdI4yD4tBXEBKSbriGujF6jnSVkJrffuo6vxACiSSxIw==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/@types/codemirror": {
|
||||
"version": "5.60.17",
|
||||
"resolved": "https://registry.npmjs.org/@types/codemirror/-/codemirror-5.60.17.tgz",
|
||||
"integrity": "sha512-AZq2FIsUHVMlp7VSe2hTfl5w4pcUkoFkM3zVsRKsn1ca8CXRDYvnin04+HP2REkwsxemuHqvDofdlhUWNpbwfw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/tern": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/estree": {
|
||||
"version": "1.0.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
|
||||
"integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/geojson": {
|
||||
"version": "7946.0.16",
|
||||
"resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz",
|
||||
@@ -555,6 +573,12 @@
|
||||
"@types/pbf": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/marked": {
|
||||
"version": "4.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/marked/-/marked-4.3.2.tgz",
|
||||
"integrity": "sha512-a79Yc3TOk6dGdituy8hmTTJXjOkZ7zsFYV10L337ttq/rec8lRMDBpV7fL3uLx6TgbFCa5DU/h8FmIBQPSbU0w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/pbf": {
|
||||
"version": "3.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/pbf/-/pbf-3.0.5.tgz",
|
||||
@@ -570,6 +594,15 @@
|
||||
"@types/geojson": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/tern": {
|
||||
"version": "0.23.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/tern/-/tern-0.23.9.tgz",
|
||||
"integrity": "sha512-ypzHFE/wBzh+BlH6rrBgS5I/Z7RD21pGhZ2rltb/+ZrVM1awdZwjx7hE5XfuYgHWk9uvV5HLZN3SloevCAp3Bw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/estree": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@xmldom/xmldom": {
|
||||
"version": "0.8.13",
|
||||
"resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.13.tgz",
|
||||
@@ -585,6 +618,21 @@
|
||||
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/codemirror": {
|
||||
"version": "5.65.21",
|
||||
"resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.65.21.tgz",
|
||||
"integrity": "sha512-6teYk0bA0nR3QP0ihGMoxuKzpl5W80FpnHpBJpgy66NK3cZv5b/d/HY8PnRvfSsCG1MTfr92u2WUl+wT0E40mQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/codemirror-spell-checker": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/codemirror-spell-checker/-/codemirror-spell-checker-1.1.2.tgz",
|
||||
"integrity": "sha512-2Tl6n0v+GJRsC9K3MLCdLaMOmvWL0uukajNJseorZJsslaxZyZMgENocPU8R0DyoTAiKsyqiemSOZo7kjGV0LQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"typo-js": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/concat-stream": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz",
|
||||
@@ -606,6 +654,19 @@
|
||||
"integrity": "sha512-X7hshQbLyMJ/3RPhyObLARM2sNxxmRALLKx1+NVFFnQ9gKzmCrxm9+uLIAdBcvc8FNLpctqlQ2V6AE92Ol9UDQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/easymde": {
|
||||
"version": "2.21.0",
|
||||
"resolved": "https://registry.npmjs.org/easymde/-/easymde-2.21.0.tgz",
|
||||
"integrity": "sha512-5uE7I/DEN8gvGRwxaqAv7h1PMEK2ykNXVX5zL0dK3nCYROGja3AMbdQz8eCEELnfvCfy7tRkTmLuvyJG8uSWjQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/codemirror": "^5.60.10",
|
||||
"@types/marked": "^4.0.7",
|
||||
"codemirror": "^5.65.15",
|
||||
"codemirror-spell-checker": "1.1.2",
|
||||
"marked": "^4.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
|
||||
@@ -683,6 +744,12 @@
|
||||
"node": ">=16"
|
||||
}
|
||||
},
|
||||
"node_modules/heic-to": {
|
||||
"version": "1.5.2",
|
||||
"resolved": "https://registry.npmjs.org/heic-to/-/heic-to-1.5.2.tgz",
|
||||
"integrity": "sha512-8Fns+lZHAWmz5U5IUxDeXKwIf3foBoKNPLxxFY4B0MkLjNuomEIHCoDbDE+x/llFK3NCEO1cu4+n3iUKY+Svmw==",
|
||||
"license": "LGPL-3.0"
|
||||
},
|
||||
"node_modules/ieee754": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
||||
@@ -796,6 +863,18 @@
|
||||
"url": "https://github.com/maplibre/maplibre-gl-js?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/marked": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz",
|
||||
"integrity": "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"marked": "bin/marked.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 12"
|
||||
}
|
||||
},
|
||||
"node_modules/minimist": {
|
||||
"version": "1.2.8",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
|
||||
@@ -906,6 +985,12 @@
|
||||
"integrity": "sha512-PIPwB1kYBnbw/ezvPBJa5dCN5qEwokfpAkI3BmpZWAwcVID4nDf1qH6WV16A2fQaJmsKx0un5S/zhxN+PQeKDQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/sortablejs": {
|
||||
"version": "1.15.7",
|
||||
"resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.15.7.tgz",
|
||||
"integrity": "sha512-Kk8wLQPlS+yi1ZEf48a4+fzHa4yxjC30M/Sr2AnQu+f/MPwvvX9XjZ6OWejiz8crBsLwSq8GHqaxaET7u6ux0A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/string_decoder": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
|
||||
@@ -936,6 +1021,12 @@
|
||||
"integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/typo-js": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/typo-js/-/typo-js-1.3.2.tgz",
|
||||
"integrity": "sha512-Z1YkJ7IIYNrFeOxAlHUercY4Q2I+PhYD/3VkWpJGy/Oqudy3bFpNcQxnv6Oa9fTSXCHPGz1eDoX1bZYm2Z891A==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/util-deprecate": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
{
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "node scripts/gen-weather-icons.js && esbuild js/src/main.js --bundle --minify --format=iife --outfile=js/main.js --loader:.woff2=file --loader:.woff=file --asset-names=../fonts/[name] && esbuild js/src/map.js --bundle --minify --format=iife --outfile=js/map.js && mkdir -p css-compiled fonts && { mv js/main.css css-compiled/main.css 2>/dev/null || true; } && { mv js/map.css css-compiled/map.css 2>/dev/null || true; }"
|
||||
"build": "node scripts/gen-weather-icons.js && esbuild js/src/main.js --bundle --minify --format=iife --outfile=js/main.js --loader:.woff2=file --loader:.woff=file --asset-names=../fonts/[name] && esbuild js/src/map.js --bundle --minify --format=iife --outfile=js/map.js && esbuild js/src/feed-actions.js --bundle --minify --format=iife --outfile=js/feed-actions.js && rm -rf js/post && esbuild js/src/post-form.js --bundle --minify --format=esm --splitting --outdir=js/post && mkdir -p css-compiled fonts && { mv js/main.css css-compiled/main.css 2>/dev/null || true; } && { mv js/map.css css-compiled/map.css 2>/dev/null || true; } && { mv js/post/post-form.css css-compiled/post-form.css 2>/dev/null || true; }"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fontsource-variable/dm-sans": "latest",
|
||||
"@fontsource/dm-serif-display": "latest",
|
||||
"@mapbox/togeojson": "^0.16.2",
|
||||
"easymde": "^2",
|
||||
"heic-to": "^1",
|
||||
"maplibre-gl": "^4",
|
||||
"photoswipe": "^5",
|
||||
"scrollama": "^3"
|
||||
"scrollama": "^3",
|
||||
"sortablejs": "^1.15.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"esbuild": "^0.21",
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
{# intotheeast override of Grav's datetime field.
|
||||
|
||||
Grav's stock forms/fields/datetime/datetime.html.twig is DEPRECATED and just
|
||||
extends the text field, so `type: datetime` renders a plain <input type="text">
|
||||
— the owner sees a raw box (the literal word "now" from `default: now`) with no
|
||||
way to pick a date/time. Here we render a native <input type="datetime-local">
|
||||
instead: a real calendar + clock picker, and an excellent one on mobile.
|
||||
|
||||
The value is prefilled to the current local time by post-form.js (JS knows the
|
||||
traveller's timezone; the server doesn't), and the existing client-side
|
||||
validator now requires this field — so an empty or invalid date can no longer
|
||||
reach the server and destroy the FilePond photo list on a re-render. #}
|
||||
{% extends "forms/field.html.twig" %}
|
||||
|
||||
{% block input_attributes %}
|
||||
type="datetime-local"
|
||||
{{ parent() }}
|
||||
{% endblock %}
|
||||
@@ -14,11 +14,20 @@
|
||||
|
||||
{% set dailies_page = grav.pages.find(trip_route ~ '/dailies') %}
|
||||
{% set stories_page = grav.pages.find(trip_route ~ '/stories') %}
|
||||
{# published-only — feeds the map, stats and counts (drafts excluded, R5) #}
|
||||
{% set journal_entries = dailies_page ? dailies_page.children.published() : [] %}
|
||||
{% set story_entries = stories_page ? stories_page.children.published() : [] %}
|
||||
|
||||
{# This branch IS the active trip, so the owner gate is just owner identity
|
||||
(KTD8). The super-admin tester authenticates too, so gate on owner_username. #}
|
||||
{% set owner_can_edit = grav.user.authenticated
|
||||
and grav.user.username == grav.config.site.owner_username %}
|
||||
{# Owner-aware feed list: owner sees drafts; everyone else published only #}
|
||||
{% set journal_feed = (owner_can_edit and dailies_page) ? dailies_page.children : journal_entries %}
|
||||
{% if owner_can_edit %}{% do assets.addJs('theme://js/feed-actions.js', {group: 'bottom'}) %}{% endif %}
|
||||
|
||||
{% set all_items = [] %}
|
||||
{% for e in journal_entries %}
|
||||
{% for e in journal_feed %}
|
||||
{% set all_items = all_items|merge([{'type': 'journal', 'page': e, 'date': e.header.date}]) %}
|
||||
{% endfor %}
|
||||
{% for s in story_entries %}
|
||||
@@ -38,7 +47,8 @@
|
||||
|
||||
{% set map_entries = [] %}
|
||||
{% for item in all_items %}
|
||||
{% if item.type == 'journal' and item.page.header.lat is not empty and item.page.header.lng is not empty %}
|
||||
{# drafts render as a feed card only — never a map marker (R5) #}
|
||||
{% if item.type == 'journal' and item.page.published and item.page.header.lat is not empty and item.page.header.lng is not empty %}
|
||||
{% set map_entries = map_entries|merge([{
|
||||
'lat': item.page.header.lat|number_format(6, '.', ''),
|
||||
'lng': item.page.header.lng|number_format(6, '.', ''),
|
||||
@@ -87,7 +97,9 @@
|
||||
has_gpx: home_gpx_urls|length > 0,
|
||||
gpx_urls: home_gpx_urls,
|
||||
gps_points: gps_points,
|
||||
show_sort: false
|
||||
show_sort: false,
|
||||
owner_can_edit: owner_can_edit,
|
||||
feed_return_url: page.url
|
||||
} only %}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
{% do assets.addJs('theme://js/main.js', {group: 'bottom'}) %}
|
||||
{% do assets.addJs('theme://js/nav.js', {group: 'bottom'}) %}
|
||||
{% block map_assets %}{% endblock %}
|
||||
{% block head_assets %}{% endblock %}
|
||||
{{ assets.css()|raw }}
|
||||
{{ assets.js()|raw }}
|
||||
</head>
|
||||
@@ -26,6 +27,10 @@
|
||||
<nav class="site-nav" id="site-nav" aria-label="Main navigation">
|
||||
<a href="{{ base_url_absolute }}"{% if page.template == 'home' %} aria-current="page"{% endif %}>Home</a>
|
||||
<a href="{{ base_url_absolute }}/trips"{% if page.template == 'trips' %} aria-current="page"{% endif %}>Past Trips</a>
|
||||
{% if grav.user.authenticated %}
|
||||
<a href="{{ base_url_absolute }}/post"{% if page.template == 'post-form' %} aria-current="page"{% endif %}>New Post</a>
|
||||
<a href="{{ base_url_absolute }}/gpx-manager"{% if page.template == 'gpx-manager' %} aria-current="page"{% endif %}>GPX Manager</a>
|
||||
{% endif %}
|
||||
</nav>
|
||||
{% endblock %}
|
||||
</header>
|
||||
|
||||
@@ -1,7 +1,22 @@
|
||||
{% include 'partials/weather-icons.html.twig' %}
|
||||
<article class="journal-post" id="entry-{{ entry.slug }}" data-type="journal" data-lat="{{ entry.header.lat }}" data-lng="{{ entry.header.lng }}">
|
||||
{% set owner_can_edit = owner_can_edit ?? false %}
|
||||
{% set feed_return_url = feed_return_url ?? trip_page.url ?? '/' %}
|
||||
<article class="journal-post{% if not entry.published %} is-draft{% endif %}" id="entry-{{ entry.slug }}" data-type="journal" data-lat="{{ entry.header.lat }}" data-lng="{{ entry.header.lng }}"{% if owner_can_edit %} data-entry-route="{{ entry.route }}"{% endif %}>
|
||||
<header class="journal-post-header">
|
||||
<h2 class="journal-post-title">{{ entry.title }}</h2>
|
||||
<div class="journal-post-titlerow">
|
||||
<h2 class="journal-post-title">{{ entry.title }}{% if not entry.published %} <span class="journal-draft-badge">Draft</span>{% endif %}</h2>
|
||||
{% if owner_can_edit %}
|
||||
<div class="journal-post-actions" data-entry-route="{{ entry.route }}">
|
||||
<a class="entry-action entry-action--edit" href="/post?edit={{ entry.route|url_encode }}&return={{ feed_return_url|url_encode }}">Edit</a>
|
||||
<button class="entry-action entry-action--delete" type="button" data-delete-start>Delete</button>
|
||||
<span class="entry-delete-confirm" hidden>
|
||||
<button class="entry-action entry-action--cancel" type="button" data-delete-cancel>Cancel</button>
|
||||
<button class="entry-action entry-action--confirm" type="button" data-delete-confirm>Confirm delete</button>
|
||||
</span>
|
||||
<span class="entry-delete-msg" role="status" aria-live="polite"></span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<p class="journal-post-meta">
|
||||
<a class="journal-post-permalink" href="{{ entry.url }}">
|
||||
<time datetime="{{ entry.date|date('Y-m-d') }}">{{ entry.date|date('d M Y')|upper }}</time>
|
||||
|
||||
@@ -5,6 +5,10 @@
|
||||
caller only. Home's active-trip include omits it (`only`), so it defaults off
|
||||
and that header renders exactly as before (KTD4 / R12). #}
|
||||
{% set trip_header_extras = trip_header_extras|default(false) %}
|
||||
{# owner_can_edit gates the Draft badge + Edit/Delete controls on each card
|
||||
(threaded into entry-journal below). Default false so any caller that doesn't
|
||||
pass it renders a read-only feed. #}
|
||||
{% set owner_can_edit = owner_can_edit ?? false %}
|
||||
<div class="home-feed-col">
|
||||
<div class="home-trip-header">
|
||||
<h1 class="home-trip-name">{{ trip_page.title }}</h1>
|
||||
|
||||
@@ -1,126 +1,28 @@
|
||||
{% extends 'default.html.twig' %}
|
||||
|
||||
{% block head_assets %}
|
||||
{# FilePond's own CSS must load from the <head>. The filepond field registers
|
||||
it via assets.addCss() during body rendering, which is too late for the
|
||||
theme's head-only {{ assets.css() }} — so the widget renders unstyled
|
||||
(giant overlapping tiles) unless we add it here. #}
|
||||
{% do assets.addCss('plugin://form/assets/filepond/filepond.min.css') %}
|
||||
{% do assets.addCss('plugin://form/assets/filepond/filepond-plugin-image-preview.min.css') %}
|
||||
{% do assets.addCss('theme://css-compiled/post-form.css') %}
|
||||
<script type="module" src="{{ url('theme://js/post/post-form.js') }}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="post-form-wrap">
|
||||
<div class="post-form-wrap" data-trip-url="{{ config.site.active_trip }}">
|
||||
<h1>New Entry</h1>
|
||||
{% include 'forms/form.html.twig' ignore missing %}
|
||||
|
||||
<div class="form-action-row">
|
||||
<button type="button" id="get-location" class="btn-action">📍 Get Location</button>
|
||||
<button type="button" id="get-weather" class="btn-action">🌤 Get Weather</button>
|
||||
<button type="button" id="get-weather" class="btn-action" disabled>🌤 Get Weather</button>
|
||||
</div>
|
||||
<p id="location-status" class="form-status"></p>
|
||||
<p id="weather-status" class="form-status"></p>
|
||||
<p id="location-status" class="form-status" role="status"></p>
|
||||
<p id="weather-status" class="form-status" role="status"></p>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Custom validation — form uses novalidate so we handle it here
|
||||
(function() {
|
||||
var REQUIRED = ['title', 'content'];
|
||||
var form = document.querySelector('form[name="new-entry"]');
|
||||
if (!form) return;
|
||||
|
||||
function clearErrors() {
|
||||
form.querySelectorAll('.field-error').forEach(function(el) { el.remove(); });
|
||||
form.querySelectorAll('.field-invalid').forEach(function(el) { el.classList.remove('field-invalid'); });
|
||||
}
|
||||
|
||||
function showError(field, msg) {
|
||||
field.classList.add('field-invalid');
|
||||
var err = document.createElement('span');
|
||||
err.className = 'field-error';
|
||||
err.textContent = msg;
|
||||
field.parentNode.insertBefore(err, field.nextSibling);
|
||||
}
|
||||
|
||||
form.addEventListener('submit', function(e) {
|
||||
clearErrors();
|
||||
var firstInvalid = null;
|
||||
REQUIRED.forEach(function(name) {
|
||||
var field = form.querySelector('[name="data[' + name + ']"]');
|
||||
if (field && !field.value.trim()) {
|
||||
showError(field, name.charAt(0).toUpperCase() + name.slice(1) + ' is required.');
|
||||
if (!firstInvalid) firstInvalid = field;
|
||||
}
|
||||
});
|
||||
if (firstInvalid) {
|
||||
e.preventDefault();
|
||||
firstInvalid.focus();
|
||||
firstInvalid.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
}
|
||||
});
|
||||
}());
|
||||
</script>
|
||||
|
||||
<script>
|
||||
var WMO_MAP = {
|
||||
0:'Sunny',1:'Partly cloudy',2:'Partly cloudy',3:'Cloudy',
|
||||
45:'Foggy',48:'Foggy',
|
||||
51:'Drizzle',53:'Drizzle',55:'Drizzle',56:'Drizzle',57:'Drizzle',
|
||||
61:'Rain',63:'Rain',65:'Rain',66:'Rain',67:'Rain',80:'Rain',81:'Rain',82:'Rain',
|
||||
71:'Snow',73:'Snow',75:'Snow',77:'Snow',85:'Snow',86:'Snow',
|
||||
95:'Thunderstorm',96:'Thunderstorm',99:'Thunderstorm'
|
||||
};
|
||||
|
||||
function getField(name) {
|
||||
return document.querySelector('input[name="data[' + name + ']"]');
|
||||
}
|
||||
|
||||
document.getElementById('get-location').addEventListener('click', function() {
|
||||
var status = document.getElementById('location-status');
|
||||
status.className = 'form-status';
|
||||
status.textContent = 'Getting location…';
|
||||
if (!navigator.geolocation) {
|
||||
status.textContent = 'Geolocation not supported.';
|
||||
return;
|
||||
}
|
||||
navigator.geolocation.getCurrentPosition(function(pos) {
|
||||
var lat = pos.coords.latitude.toFixed(6);
|
||||
var lng = pos.coords.longitude.toFixed(6);
|
||||
var latField = getField('lat');
|
||||
var lngField = getField('lng');
|
||||
if (latField) latField.value = lat;
|
||||
if (lngField) lngField.value = lng;
|
||||
status.textContent = '✓ Location captured · ' + lat + ', ' + lng;
|
||||
status.classList.add('form-status--ok');
|
||||
}, function(err) {
|
||||
status.textContent = '✗ Could not get location: ' + err.message;
|
||||
status.classList.add('form-status--err');
|
||||
});
|
||||
});
|
||||
|
||||
document.getElementById('get-weather').addEventListener('click', function() {
|
||||
var status = document.getElementById('weather-status');
|
||||
status.className = 'form-status';
|
||||
var latField = getField('lat');
|
||||
var lngField = getField('lng');
|
||||
var lat = latField ? latField.value.trim() : '';
|
||||
var lng = lngField ? lngField.value.trim() : '';
|
||||
if (!lat || !lng) {
|
||||
status.textContent = 'Get location first, then fetch weather.';
|
||||
return;
|
||||
}
|
||||
status.textContent = 'Fetching weather…';
|
||||
var url = 'https://api.open-meteo.com/v1/forecast?latitude=' + lat +
|
||||
'&longitude=' + lng +
|
||||
'¤t=temperature_2m,weather_code&temperature_unit=celsius';
|
||||
fetch(url)
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(data) {
|
||||
var temp = Math.round(data.current.temperature_2m);
|
||||
var code = data.current.weather_code;
|
||||
var desc = WMO_MAP[code] || 'Cloudy';
|
||||
var tempField = getField('weather_temp_c');
|
||||
var descField = getField('weather_desc');
|
||||
if (tempField) tempField.value = temp;
|
||||
if (descField) descField.value = desc;
|
||||
status.textContent = '✓ Weather set · ' + desc + ' · ' + temp + '°C';
|
||||
status.classList.add('form-status--ok');
|
||||
})
|
||||
.catch(function() {
|
||||
status.textContent = '✗ Could not fetch weather — enter manually if needed.';
|
||||
status.classList.add('form-status--err');
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{# Get Location / Get Weather, disclosure, and validation all live in the
|
||||
page-scoped bundle (js/src/post-form.js), loaded via head_assets above. #}
|
||||
{% endblock %}
|
||||
|
||||
@@ -9,11 +9,25 @@
|
||||
{% endblock %}
|
||||
{% set dailies_page = grav.pages.find(page.route ~ '/dailies') %}
|
||||
{% set stories_page = grav.pages.find(page.route ~ '/stories') %}
|
||||
{# journal_entries stays published-only — it feeds the map, stats and counts,
|
||||
which must never include drafts (R5). #}
|
||||
{% set journal_entries = dailies_page ? dailies_page.children.published() : [] %}
|
||||
{% set story_entries = stories_page ? stories_page.children.published() : [] %}
|
||||
|
||||
{# Owner gate (KTD8): the site owner (not merely any login — the super-admin
|
||||
tester also authenticates) viewing the ACTIVE trip. Drives draft visibility
|
||||
in the feed and the Edit/Delete controls (threaded to the card partial). #}
|
||||
{% set active_trip_slug = (grav.config.site.active_trip|default(''))|split('/')|last %}
|
||||
{% set owner_can_edit = grav.user.authenticated
|
||||
and grav.user.username == grav.config.site.owner_username
|
||||
and page.slug == active_trip_slug %}
|
||||
{# Feed list is owner-aware: the owner sees drafts (unpublished) too; everyone
|
||||
else (and every non-active-trip view) sees published only (R5, KTD7). #}
|
||||
{% set journal_feed = (owner_can_edit and dailies_page) ? dailies_page.children : journal_entries %}
|
||||
{% if owner_can_edit %}{% do assets.addJs('theme://js/feed-actions.js', {group: 'bottom'}) %}{% endif %}
|
||||
|
||||
{% set all_items = [] %}
|
||||
{% for e in journal_entries %}
|
||||
{% for e in journal_feed %}
|
||||
{% set all_items = all_items|merge([{'type': 'journal', 'page': e, 'date': e.header.date}]) %}
|
||||
{% endfor %}
|
||||
{% for s in story_entries %}
|
||||
@@ -41,7 +55,8 @@
|
||||
|
||||
{% set map_entries = [] %}
|
||||
{% for item in all_items %}
|
||||
{% if item.page.header.lat is not empty and item.page.header.lng is not empty %}
|
||||
{# drafts render as a feed card only — never a map marker (R5) #}
|
||||
{% if item.page.published and item.page.header.lat is not empty and item.page.header.lng is not empty %}
|
||||
{% set map_entries = map_entries|merge([{
|
||||
'type': item.type,
|
||||
'lat': item.page.header.lat|number_format(6, '.', ''),
|
||||
@@ -79,7 +94,9 @@
|
||||
gpx_urls: gpx_urls,
|
||||
gps_points: gps_points,
|
||||
show_sort: true,
|
||||
trip_header_extras: true
|
||||
trip_header_extras: true,
|
||||
owner_can_edit: owner_can_edit,
|
||||
feed_return_url: page.url
|
||||
} only %}
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user