import { marked } from 'marked';
import { selectedDate } from './calendarStore';
import { getTranslate } from '@tolgee/svelte';
+ import { onMount } from 'svelte';
const { t } = getTranslate();
let { log } = $props();
- let preview;
- let content;
let modal;
- let isModalOpen = $state(false);
+ let modalInstance;
+
+ onMount(() => {
+ // Import Bootstrap Modal
+ import('bootstrap').then((bootstrap) => {
+ modalInstance = new bootstrap.Modal(modal, {
+ backdrop: true,
+ keyboard: true
+ });
+ });
+ });
function openModal() {
- if (!preview || !modal || !content) return;
-
- const previewRect = preview.getBoundingClientRect();
- const targetWidth = Math.min(window.innerWidth * 0.8, 600); // Target width
-
- // Initial state for the animation
- // Position and scale to match the button
- content.style.left = `${previewRect.left}px`;
- content.style.top = `${previewRect.top}px`;
- content.style.width = `${previewRect.width}px`;
- content.style.height = `${previewRect.height}px`;
- content.style.transform = 'scale(1)'; // Start at button's scale
- content.style.opacity = '0';
-
- modal.style.display = 'flex';
-
- void content.offsetWidth;
-
- // Target state for the animation
- // Calculate scale factor to reach targetWidth from previewRect.width
- const scaleX = targetWidth / previewRect.width;
-
- const targetLeft = (window.innerWidth - targetWidth) / 2;
- const targetTop = window.innerHeight * 0.2;
-
- content.style.left = `${targetLeft}px`; // Position for final state
- content.style.top = `${targetTop}px`; // Position for final state
- content.style.width = `${targetWidth}px`;
- // Height will be 'auto' or controlled by max-height in CSS
- content.style.height = 'auto'; // Let CSS max-height handle this
- content.style.transform = 'scale(1)'; // End at normal scale, but at new position/size
- content.style.opacity = '1';
-
- isModalOpen = true;
- document.body.style.overflow = 'hidden';
- }
-
- function closeModal() {
- if (!preview || !modal || !content) return;
-
- const previewRect = preview.getBoundingClientRect();
-
- content.style.left = `${previewRect.left}px`;
- content.style.top = `${previewRect.top}px`;
- content.style.width = `${previewRect.width}px`;
- content.style.height = `${previewRect.height}px`;
- content.style.transform = 'scale(1)';
- content.style.opacity = '0';
-
- setTimeout(() => {
- if (!isModalOpen) {
- modal.style.display = 'none';
- document.body.style.overflow = '';
- }
- }, 400);
-
- isModalOpen = false;
+ if (modalInstance) {
+ modalInstance.show();
+ }
}
- function handleKeydown(event) {
- if (event.key === 'Escape' && isModalOpen) {
- closeModal();
+ function goToDate() {
+ $selectedDate = { year: log.year, month: log.month, day: log.day };
+ if (modalInstance) {
+ modalInstance.hide();
}
}
</script>
-<!-- svelte-ignore a11y_click_events_have_key_events -->
-<svelte:window on:keydown={handleKeydown} />
-
<!-- svelte-ignore a11y_consider_explicit_label -->
-<button
- bind:this={preview}
- onclick={openModal}
- id="zoomButton"
- class="btn"
- style="width: 200px; height: 100px;"
->
+<button onclick={openModal} id="zoomButton" class="btn" style="width: 200px; height: 100px;">
<div class="d-flex flex-row h-100">
<div class="left d-flex flex-column justify-content-evenly px-1">
<div><b>{log?.year}</b></div>
- <div><em><b>{log?.years_old}</b> J</em></div>
+ <div><em><b>{log?.years_old}</b> {$t('aLookBack.Year_one_letter')}</em></div>
</div>
<div class="html-preview p-1">
{@html marked.parse(log?.text)}
</div>
</button>
-<!-- svelte-ignore a11y_no_static_element_interactions -->
-<!-- svelte-ignore a11y_click_events_have_key_events -->
+<!-- Standard Bootstrap Modal -->
<div
bind:this={modal}
- id="zoomModal"
- class="zoom-modal"
- onclick={(event) => {
- if (event.target === modal) {
- closeModal();
- }
- }}
+ class="modal fade"
+ tabindex="-1"
+ aria-labelledby="alookbackModalLabel"
+ aria-hidden="true"
>
- <div bind:this={content} class="zoom-content">
- <div class="zoom-content-header">
- <span
- >{$t('aLookBack.header_X_years_ago', { years_old: log?.years_old })} | {new Date(
- log?.year,
- log?.month - 1,
- log?.day
- ).toLocaleDateString('locale', {
- weekday: 'long',
- day: '2-digit',
- month: '2-digit',
- year: 'numeric'
- })}</span
- >
- <button
- type="button"
- class="btn-close btn-close-white"
- aria-label="Close"
- onclick={closeModal}
- ></button>
+ <div class="modal-dialog modal-dialog-centered modal-dialog-scrollable modal-lg">
+ <div class="modal-content">
+ <div class="modal-header">
+ <h5 class="modal-title" id="alookbackModalLabel">
+ {$t('aLookBack.header_X_years_ago', { years_old: log?.years_old })} | {new Date(
+ log?.year,
+ log?.month - 1,
+ log?.day
+ ).toLocaleDateString('locale', {
+ weekday: 'long',
+ day: '2-digit',
+ month: '2-digit',
+ year: 'numeric'
+ })}
+ </h5>
+ <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
+ </div>
+ <div class="modal-body">
+ {@html marked.parse(log?.text)}
+ </div>
+ <div class="modal-footer">
+ <button onclick={goToDate} class="btn btn-primary">
+ {$t('aLookBack.open')}
+ </button>
+ </div>
</div>
- <div class="modal-text">{@html marked.parse(log?.text)}</div>
- <button
- onclick={() => {
- $selectedDate = { year: log.year, month: log.month, day: log.day };
- closeModal();
- }}
- class="btn btn-primary"
- id="closeZoom">{$t('aLookBack.open')}</button
- >
</div>
</div>
<style>
.left {
- background-color: rgba(180, 180, 180, 0.45);
border-top-left-radius: 0.375rem;
border-bottom-left-radius: 0.375rem;
}
- .modal-text {
- margin-left: 20px;
- margin-right: 20px;
- margin-top: 20px;
- }
-
- #closeZoom {
- margin-left: 20px;
- margin-bottom: 20px;
- }
-
- .zoom-content-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding: 8px 15px;
- background-color: #343a40;
- color: white;
- border-bottom: 1px solid #495057;
- flex-shrink: 0;
- border-top-left-radius: 8px;
- border-top-right-radius: 8px;
- }
-
.html-preview :global(h1),
.html-preview :global(h2),
.html-preview :global(h3),
left: 0;
right: 0;
height: 40px;
- background: linear-gradient(to bottom, transparent, rgba(219, 219, 219, 0.45) 80%);
pointer-events: none;
}
+ :global(body[data-bs-theme='dark']) .html-preview::after {
+ background: linear-gradient(to bottom, transparent, rgba(80, 80, 80, 0.45) 80%);
+ }
+
+ :global(body[data-bs-theme='light']) .html-preview::after {
+ background: linear-gradient(to bottom, transparent, rgba(219, 219, 219, 0.45) 80%);
+ }
+
+ :global(body[data-bs-theme='dark']) #zoomButton {
+ background-color: rgba(138, 138, 138, 0.45);
+ color: #ececec;
+ }
+
+ :global(body[data-bs-theme='dark']) .left {
+ background-color: rgba(141, 141, 141, 0.45);
+ }
+
+ :global(body[data-bs-theme='light']) .left {
+ background-color: rgba(180, 180, 180, 0.45);
+ }
+
#zoomButton {
background-color: rgba(219, 219, 219, 0.45);
transition: 0.3s ease;
background-color: rgba(219, 219, 219, 0.65);
}
- .zoom-modal {
- position: fixed;
- top: 0;
- left: 0;
- width: 100vw;
- height: 100vh;
- display: none;
- justify-content: center;
- align-items: center;
- z-index: 1050;
- background-color: rgba(0, 0, 0, 0.5);
- overflow: hidden;
+ .modal-header {
+ border-bottom: none;
}
- .zoom-content {
- position: absolute;
- background: white;
- color: black;
- border-radius: 8px;
- box-shadow: 0 0 20px rgba(0, 0, 0, 0.4);
- /* transform-origin: top left; */ /* Let's try center for smoother scaling if we use scale for size */
- transition:
- left 0.4s cubic-bezier(0.25, 0.8, 0.25, 1),
- top 0.4s cubic-bezier(0.25, 0.8, 0.25, 1),
- width 0.4s cubic-bezier(0.25, 0.8, 0.25, 1),
- height 0.4s cubic-bezier(0.25, 0.8, 0.25, 1),
- /* Animating height can be jittery */ transform 0.4s cubic-bezier(0.25, 0.8, 0.25, 1),
- opacity 0.4s ease-out;
- /* padding: 20px; */
- overflow-y: auto;
- max-width: 600px; /* Set max-width here */
- max-height: 80vh; /* Set max-height here */
- /* Consider adding will-change for properties you animate, but use sparingly */
- /* will-change: transform, opacity, left, top, width, height; */
+ .modal-footer {
+ border-top: none;
}
</style>
];
</script>
-<div class="datepicker glassLight">
+<div class="datepicker glass">
<div class="datepicker-header">
<button type="button" class="btn btnLeftRight" onclick={() => changeMonth(-1)}><</button>
<div class="date-selectors">
font-weight: bold;
padding: 8px 0;
font-size: 0.9em;
+ }
+
+ :global(body[data-bs-theme='light']) .day-header {
color: #666;
}
+
+ :global(body[data-bs-theme='light']) .day-header {
+ color: #5c5c5c;
+ }
+
.day {
height: 32px;
width: 32px;
user-select: none;
--dot-color: rgb(250, 199, 58);
}
+ :global(body[data-bs-theme='light']) .day {
+ color: #222;
+ }
.day:hover {
background: #f0f0f0;
+ color: black;
}
.day.mark-background {
background-color: #00ad00;
color: white;
aspect-ratio: 1;
}
+ .day.mark-background:hover {
+ background-color: #008a00;
+ }
.day.mark-circle {
- /* background-color: transparent;*/
border: 3px solid #f57c00;
- /* color: #ff9224; */
}
.day.mark-dot::after {
// Wait for darkMode and language to be initialized before creating picker
$effect(() => {
- console.log('EmojiMart effect:', {
- darkMode: $darkMode,
- emojiPickerEl: !!emojiPickerEl,
- picker: !!picker
- });
if ($darkMode !== undefined && emojiPickerEl && !picker) {
createPicker();
}
});
function createPicker() {
- console.log('Creating emoji picker with theme:', $darkMode ? 'dark' : 'light');
picker = new Picker({
theme: $darkMode ? 'dark' : 'light',
autoFocus: true,
<div class="search d-flex flex-column">
<form onsubmit={searchForString} class="input-group">
<button
- class="btnSearchPopover btn btn-outline-secondary glassLight"
+ class="btnSearchPopover btn btn-outline-secondary glass"
data-bs-toggle="popover"
data-bs-title="Suche"
data-bs-content={$t('search.description')}
setTimeout(() => (showTagDropdown = false), 150);
}}
/>
- <button class="btn btn-outline-secondary glassLight" type="submit" id="search-button">
+ <button class="btn btn-outline-secondary glass" type="submit" id="search-button">
{#if $isSearching}
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
{:else}
{/if}
</div>
{/if}
- <div class="list-group flex-grow-1 mb-2 glassLight">
+ <div class="list-group flex-grow-1 mb-2 glass">
{#if $searchResults.length > 0}
{#each $searchResults as result}
<button
.noResult {
font-size: 25pt;
font-weight: 750;
- color: #ccc;
text-align: center;
padding: 1rem;
user-select: none;
}
+ :global(body[data-bs-theme='dark']) .noResult {
+ color: #757575;
+ }
+
+ :global(body[data-bs-theme='light']) .noResult {
+ color: #cccccc;
+ }
+
:global(.popover-body > span) {
font-family: monospace;
border: 1px solid #ccc;
background-color: transparent;
border: 1px solid #ccc;
border-radius: 5px;
- color: #495057;
+
cursor: pointer;
font-size: 11pt;
margin-left: 0.3rem;
transition: all 0.3s ease;
}
+ :global(body[data-bs-theme='dark']) .removeBtn {
+ color: #c2c2c2;
+ }
+
+ :global(body[data-bs-theme='light']) .removeBtn {
+ color: #495057;
+ }
+
.removeBtn:hover {
- color: #dc3545;
+ color: #dc3545 !important;
+ }
+
+ .modal-header {
+ border-bottom: none;
+ }
+
+ .modal-footer {
+ border-top: none;
}
</style>
<!-- Admin status bar -->
<div
- class="d-flex align-items-center mb-4 p-3 bg-success bg-opacity-10 border border-success rounded"
+ class="d-flex align-items-center mb-4 p-3 bg-success bg-opacity-25 border border-success rounded"
>
- <span class="text-success me-3">🔓 {$t('settings.admin.authenticated')}</span>
+ <span class="text-success me-3">🔓 {$t('settings.admin.authorized')} </span>
<button class="btn btn-outline-secondary btn-sm ms-2" onclick={resetAdminState}>
{$t('settings.admin.logout')}
</button>
<style>
.settings-admin {
- min-height: 40vh;
+ min-height: 65vh;
}
.table th {
if (to < from) to = from; // ensure non-inverted
return { level: s.level, from, to };
});
+
+ // Fix overlapping ranges by making them exclusive/inclusive properly
+ for (let i = 1; i < legendRanges.length; i++) {
+ // Make sure current segment doesn't start where previous ends
+ if (legendRanges[i].from === legendRanges[i - 1].to && legendRanges[i].from > 0) {
+ legendRanges[i].from = legendRanges[i - 1].to + 1;
+ }
+ }
}
const colorLevel = (wc) => {
{:else if years.length !== 0}
<div class="year-selector d-flex align-items-center gap-2 mb-3 flex-wrap">
<button
- class="btn btn-sm btn-outline-secondary"
+ class="btn btn-sm btn-secondary nav-button"
onclick={prevYear}
disabled={years.indexOf(selectedYear) === 0}
- aria-label="previous year">«</button
+ aria-label="previous year"><</button
>
<select
class="form-select form-select-sm year-dropdown"
{/each}
</select>
<button
- class="btn btn-sm btn-outline-secondary"
+ class="btn btn-sm btn-secondary nav-button"
onclick={nextYear}
disabled={years.indexOf(selectedYear) === years.length - 1}
- aria-label="next year">»</button
+ aria-label="next year">></button
>
<div class="legend ms-auto d-flex align-items-center gap-1">
<span class="legend-label small">{$t('settings.statistics.legend')}</span>
return dayCount[maxIndex] > 0 ? weekdays[maxIndex] : '🤷♂️';
})()}
</li>
- <li>
- 📖 {$t('settings.statistics.bookpages', {
- pages: Math.round(dayStats.reduce((sum, d) => sum + d.wordCount, 0) / 300)
- })}
- </li>
<li>
🎯 {(() => {
if (dayStats.length === 0) return '0%';
return $t('settings.statistics.activityRate', { percent: activityRate });
})()}
</li>
+ <li>
+ 📖 {$t('settings.statistics.bookpages', {
+ pages: Math.round(dayStats.reduce((sum, d) => sum + d.wordCount, 0) / 300)
+ })}
+ </li>
</ul>
{:else if years.length === 0}
<p class="text-info">{$t('settings.statistics.no_data')}</p>
</div>
<style>
+ :global(body[data-bs-theme='dark']) .nav-button {
+ color: #bebebe;
+ }
+
+ .nav-button:disabled {
+ opacity: 0.5;
+ }
+
.headerTotal {
margin-top: 1rem;
margin-bottom: 0.5rem;
}
.settings-stats {
- min-height: 40vh;
+ min-height: 65vh;
}
+
.year-selector .year-dropdown {
width: auto;
}
background: transparent;
}
/* Color scale (adjust to theme) */
- .level-0 {
+
+ :global(body[data-bs-theme='light']) .level-0 {
background: var(--heatmap-empty, #ebedf0);
}
+
+ :global(body[data-bs-theme='dark']) .level-0 {
+ background: var(--heatmap-empty, #333333);
+ }
+
.level-1 {
background: #c6e48b;
}
background: #196127;
}
.bookmark {
- font-size: 9px;
+ font-size: 10px;
line-height: 1;
- color: #fff;
- text-shadow: 0 0 2px rgba(0, 0, 0, 0.6);
+ color: #000;
+ text-shadow: none;
+ }
+ :global(body[data-bs-theme='dark']) .day-cell.level-0 .bookmark {
+ color: #bbb;
}
- .day-cell.level-0 .bookmark {
+ :global(body[data-bs-theme='light']) .day-cell.level-0 .bookmark {
color: #555;
- text-shadow: none;
}
+ :global(body[data-bs-theme='light']) .day-cell.level-4 .bookmark {
+ color: #dddddd;
+ }
+
/* Popover styling (applies inside Bootstrap popover) */
:global(.popover-day-content) {
min-width: 200px;
// Active sub-view of settings modal: 'settings' | 'stats' | 'admin'
let activeSettingsView = $state('settings');
+ // References for sliding indicator
+ let settingsTabGroup;
+ let settingsButton;
+ let statsButton;
+ let adminButton;
+
+ // Calculate slide offset and width for the indicator
+ function getSlideOffset(activeView) {
+ if (!settingsTabGroup || !settingsButton) return 0;
+
+ const container = settingsTabGroup;
+ const containerRect = container.getBoundingClientRect();
+
+ let activeButton;
+ switch (activeView) {
+ case 'settings':
+ activeButton = settingsButton;
+ break;
+ case 'stats':
+ activeButton = statsButton;
+ break;
+ case 'admin':
+ activeButton = adminButton;
+ break;
+ default:
+ activeButton = settingsButton;
+ }
+
+ if (!activeButton) return 0;
+
+ const buttonRect = activeButton.getBoundingClientRect();
+ // Add the container's scrollLeft to account for horizontal scrolling
+ return buttonRect.left - containerRect.left + container.scrollLeft;
+ }
+
+ function getSlideWidth(activeView) {
+ let activeButton;
+ switch (activeView) {
+ case 'settings':
+ activeButton = settingsButton;
+ break;
+ case 'stats':
+ activeButton = statsButton;
+ break;
+ case 'admin':
+ activeButton = adminButton;
+ break;
+ default:
+ activeButton = settingsButton;
+ }
+
+ return activeButton ? activeButton.offsetWidth : 0;
+ }
+
+ // Force indicator update when activeSettingsView changes or when modal is shown
+ let indicatorNeedsUpdate = $state(0);
+
+ /* $effect(() => {
+ // Trigger when activeSettingsView changes
+ activeSettingsView;
+ // Trigger a re-render to update indicator position
+ setTimeout(() => {
+ indicatorNeedsUpdate++;
+ }, 10);
+ }); */
+
// Function to compare version strings (semver-like)
function compareVersions(v1, v2) {
if (!v1 || !v2) return 0;
activeSettingsSection = 'appearance';
// Short timeout to allow layout calculation before reading offsets
setTimeout(initSettingsScrollSpy, 100);
+ // Update indicator position after modal is fully shown
+ setTimeout(() => {
+ indicatorNeedsUpdate++;
+ }, 50);
}
};
modalEl.addEventListener('shown.bs.modal', onShown);
class="modal-dialog modal-dialog-scrollable modal-dialog-centered modal-xl modal-fullscreen-sm-down"
>
<!-- -->
- <div class="modal-content shadow-lg glass">
+ <div class="modal-content shadow-lg glass glass-modal">
<div class="modal-header flex-wrap gap-2">
<div class="d-flex w-100 align-items-center">
<div
- class="btn-group flex-grow-1 overflow-auto"
+ class="btn-group flex-grow-1 overflow-auto position-relative"
+ id="settingsTabGroup"
role="group"
aria-label="Settings views"
style="scrollbar-width: none; -ms-overflow-style: none;"
+ bind:this={settingsTabGroup}
>
+ <!-- Sliding indicator -->
+ <div
+ class="sliding-indicator"
+ style="transform: translateX({indicatorNeedsUpdate &&
+ getSlideOffset(activeSettingsView)}px); width: {indicatorNeedsUpdate &&
+ getSlideWidth(activeSettingsView)}px;"
+ ></div>
+
<button
type="button"
class="btn btn-outline-primary flex-shrink-0 {activeSettingsView === 'settings'
? 'active'
: ''}"
onclick={switchToSettingsTab}
+ bind:this={settingsButton}
>
{$t('settings.title')}
</button>
? 'active'
: ''}"
onclick={switchToStatsTab}
+ bind:this={statsButton}
>
{$t('settings.statistics.title')}
</button>
? 'active'
: ''}"
onclick={switchToAdminTab}
+ bind:this={adminButton}
>
{$t('settings.admin.title')}
</button>
? 'active'
: ''}"
onclick={() => scrollToSection('appearance')}
- >{$t('settings.appearance')}</button
+ >🎨 {$t('settings.appearance')}</button
>
<button
type="button"
class="nav-link mb-1 text-start {activeSettingsSection === 'functions'
? 'active'
: ''}"
- onclick={() => scrollToSection('functions')}>{$t('settings.functions')}</button
+ onclick={() => scrollToSection('functions')}
+ >🛠️ {$t('settings.functions')}</button
>
<button
type="button"
class="nav-link mb-1 text-start {activeSettingsSection === 'tags'
? 'active'
: ''}"
- onclick={() => scrollToSection('tags')}>{$t('settings.tags')}</button
+ onclick={() => scrollToSection('tags')}>#️⃣ {$t('settings.tags')}</button
>
<button
type="button"
class="nav-link mb-1 text-start {activeSettingsSection === 'templates'
? 'active'
: ''}"
- onclick={() => scrollToSection('templates')}>{$t('settings.templates')}</button
+ onclick={() => scrollToSection('templates')}
+ >📝 {$t('settings.templates')}</button
>
<button
type="button"
class="nav-link mb-1 text-start {activeSettingsSection === 'data'
? 'active'
: ''}"
- onclick={() => scrollToSection('data')}>{$t('settings.data')}</button
+ onclick={() => scrollToSection('data')}>📁 {$t('settings.data')}</button
>
<button
type="button"
class="nav-link mb-1 text-start {activeSettingsSection === 'security'
? 'active'
: ''}"
- onclick={() => scrollToSection('security')}>{$t('settings.security')}</button
+ onclick={() => scrollToSection('security')}>🔒 {$t('settings.security')}</button
+ >
+ <button
+ type="button"
+ class="nav-link mb-1 text-start {activeSettingsSection === 'account'
+ ? 'active'
+ : ''}"
+ onclick={() => scrollToSection('account')}>👤 {$t('settings.account')}</button
>
<button
type="button"
: ''}"
onclick={() => scrollToSection('about')}
>
- {$t('settings.about')}
+ 💡 {$t('settings.about')}
{#if updateAvailable}
<Fa icon={faCircleUp} size="1.2x" class="text-info" />
{/if}
</div>
</div>
</div>
- <div id="loginonreload">
- {#if $tempSettings.requirePasswordOnPageLoad !== $settings.requirePasswordOnPageLoad}
- {@render unsavedChanges()}
- {/if}
-
- <h5>🔒 {$t('settings.reauth.title')}</h5>
- {$t('settings.reauth.description')}
-
- <div class="form-check form-switch mt-2">
- <input
- class="form-check-input"
- bind:checked={$tempSettings.requirePasswordOnPageLoad}
- type="checkbox"
- role="switch"
- id="requirePasswordOnPageLoadSwitch"
- />
- <label class="form-check-label" for="requirePasswordOnPageLoadSwitch">
- {$t('settings.reauth.label')}
- </label>
- </div>
- </div>
</div>
<div id="tags">
{/if}
</button>
</div>
- <div><h5>Import</h5></div>
</div>
<div id="security">
</div>
{/if}
</div>
+ <div id="loginonreload">
+ {#if $tempSettings.requirePasswordOnPageLoad !== $settings.requirePasswordOnPageLoad}
+ {@render unsavedChanges()}
+ {/if}
+
+ <h5>{$t('settings.reauth.title')}</h5>
+ {$t('settings.reauth.description')}
+
+ <div class="form-check form-switch mt-2">
+ <input
+ class="form-check-input"
+ bind:checked={$tempSettings.requirePasswordOnPageLoad}
+ type="checkbox"
+ role="switch"
+ id="requirePasswordOnPageLoadSwitch"
+ />
+ <label class="form-check-label" for="requirePasswordOnPageLoadSwitch">
+ {$t('settings.reauth.label')}
+ </label>
+ </div>
+ </div>
+ </div>
+
+ <div id="account">
+ <h3 class="text-primary">👤 {$t('settings.account')}</h3>
+
<div>
<h5>{$t('settings.change_username')}</h5>
<div class="form-text">
</div>
{/if}
</div>
+
<div>
<h5>{$t('settings.delete_account')}</h5>
<p>
</div>
<style>
+ #settingsTabGroup > button {
+ transition: text-decoration 0.3s ease;
+ }
+ :global(body[data-bs-theme='dark']) #settingsTabGroup > button {
+ color: white;
+ }
+ :global(body[data-bs-theme='light']) #settingsTabGroup > button {
+ color: black;
+ }
+
+ #settingsTabGroup > button.active {
+ text-decoration-color: #f57c00;
+ text-decoration-thickness: 3px;
+ }
+
+ :global(body[data-bs-theme='light']) #settingsTabGroup {
+ background-color: #b8b8b8;
+ }
+
.dailytxt {
color: #f57c00;
font-size: 1.8rem;
border-top-right-radius: 10px;
padding-left: 0.5rem;
margin-bottom: 0.5rem;
+ color: black;
}
.unsaved-changes::before {
}
#settings-content > div > div {
- background-color: #bdbdbd5d;
padding: 0.5rem;
border-radius: 10px;
margin-bottom: 1rem;
}
+ :global(body[data-bs-theme='dark']) #settings-content > div > div {
+ background-color: #8080805d;
+ }
+ :global(body[data-bs-theme='light']) #settings-content > div > div {
+ background-color: #dfdfdf5d;
+ }
h3.text-primary {
font-weight: 700;
}
.modal-header {
- border-bottom: 1px solid rgba(255, 255, 255, 0.2);
+ border-bottom: none;
}
.modal-footer {
- border-top: 1px solid rgba(255, 255, 255, 0.2);
+ border-top: none;
}
/* Custom ScrollSpy styles */
border-color 0.25s ease;
will-change: background-color, color, border-color;
}
+ :global(body[data-bs-theme='dark']) .custom-scrollspy-nav .nav-link {
+ color: #9ac2ff;
+ }
+ :global(body[data-bs-theme='light']) .custom-scrollspy-nav .nav-link {
+ color: #0c6dff;
+ }
+
.custom-scrollspy-nav .nav-link.active {
- background-color: rgba(13, 110, 253, 0.15);
- color: #0d6efd;
font-weight: 600;
+ }
+ :global(body[data-bs-theme='dark']) .custom-scrollspy-nav .nav-link.active {
+ background-color: rgba(116, 116, 116, 0.521);
+ color: #62a1ff;
+ border-left-color: #0d6efd;
+ }
+ :global(body[data-bs-theme='light']) .custom-scrollspy-nav .nav-link.active {
+ background-color: rgba(13, 110, 253, 0.1);
+ color: #0066ff;
border-left-color: #0d6efd;
}
.custom-scrollspy-nav .nav-link:not(.active):hover {
scrollbar-width: none;
-ms-overflow-style: none;
}
+
+ /* Sliding indicator for settings tabs */
+ .sliding-indicator {
+ position: absolute;
+ top: 0;
+ height: 100%;
+ background-color: var(--bs-primary);
+ border-radius: 0.375rem;
+ transition:
+ transform 0.3s cubic-bezier(0.4, 0, 0.2, 1),
+ width 0.3s cubic-bezier(0.4, 0, 0.2, 1);
+ z-index: 0;
+ pointer-events: none;
+ }
+
+ /* Ensure buttons are above the indicator */
+ .btn-group .btn {
+ position: relative;
+ z-index: 1;
+ background-color: transparent !important;
+ border-color: transparent !important;
+ }
+
+ /* Active button styling - remove background since indicator handles it */
+ .btn-group .btn.active {
+ background-color: transparent !important;
+ border-color: transparent !important;
+ color: white !important;
+ }
+
+ /* Hover effect */
+ .btn-group .btn:hover {
+ background-color: rgba(13, 110, 253, 0.1) !important;
+ border-color: transparent !important;
+ }
</style>
{#each logs as log (log.day)}
<!-- Log-Area -->
{#if ('text' in log && log.text !== '') || log.tags?.length > 0 || log.files?.length > 0}
- <div class="log mb-3 p-3 d-flex flex-row" data-log-day={log.day}>
+ <div class="log glass mb-3 p-3 d-flex flex-row" data-log-day={log.day}>
<div class="date me-3 d-flex flex-column align-items-center">
<p class="dateNumber">{log.day}</p>
<p class="dateDay">
}
.log {
- backdrop-filter: blur(10px) saturate(150%);
- background-color: rgba(199, 199, 201, 0.329);
border-radius: 15px;
- border: 1px solid rgba(223, 221, 221, 0.658);
+ }
+
+ :global(body[data-bs-theme='dark']) .glass {
+ background-color: rgba(68, 68, 68, 0.6) !important;
+ }
+
+ :global(body[data-bs-theme='light']) .glass {
+ background-color: rgba(122, 122, 122, 0.6) !important;
+ color: rgb(19, 19, 19);
}
.dateNumber {
let filteredTags = $state([]);
let selectedTags = $state([]);
+ // Action: portal dropdown to <body> and position it under the input
+ function portalDropdown(node, params) {
+ let anchorEl;
+
+ function getAnchor() {
+ if (params?.anchor) {
+ return typeof params.anchor === 'string'
+ ? document.querySelector(params.anchor)
+ : params.anchor;
+ }
+ return document.getElementById('tag-input');
+ }
+
+ function position() {
+ if (!anchorEl) return;
+ const rect = anchorEl.getBoundingClientRect();
+ node.style.position = 'fixed';
+ node.style.top = rect.bottom + 'px';
+ node.style.left = rect.left + 'px';
+ /* node.style.width = rect.width + 'px'; */
+ // keep within viewport horizontally (basic guard)
+ const maxLeft = Math.max(8, Math.min(rect.left, window.innerWidth - node.offsetWidth - 8));
+ node.style.left = maxLeft + 'px';
+ }
+
+ function attach() {
+ // move element into body so it's not clipped by ancestors and backdrop-filter works as expected
+ document.body.appendChild(node);
+ position();
+ }
+
+ function onScroll() {
+ position();
+ }
+ function onResize() {
+ position();
+ }
+
+ anchorEl = getAnchor();
+ attach();
+ // use capture to react to scrolls on any ancestor
+ window.addEventListener('scroll', onScroll, true);
+ window.addEventListener('resize', onResize);
+
+ return {
+ update(newParams) {
+ params = newParams;
+ anchorEl = getAnchor();
+ position();
+ },
+ destroy() {
+ window.removeEventListener('scroll', onScroll, true);
+ window.removeEventListener('resize', onResize);
+ // Do not manually remove node; Svelte will detach it.
+ }
+ };
+ }
+
// show the correct tags in the dropdown
$effect(() => {
if ($tags.length === 0) {
<!-- Center -->
<div class="d-flex flex-column pt-4 px-4 flex-grow-1" id="middle">
<!-- Input-Area -->
- <div class="d-flex flex-row textAreaHeader glassLight">
+ <div class="d-flex flex-row textAreaHeader glass">
<div class="flex-fill textAreaDate">
{new Date(
Date.UTC($selectedDate.year, $selectedDate.month - 1, $selectedDate.day)
</button>
</div>
{#if showTagDropdown}
- <div id="tagDropdown">
+ <div id="tagDropdown" use:portalDropdown>
{#if filteredTags.length === 0}
<em style="padding: 0.2rem;">{$t('tags.no_tags_found')}</em>
{:else}
white-space: nowrap;
}
- .tag-item.selected {
- background-color: #b2b4b6;
- }
-
.selectedTags {
margin-top: 0.5rem;
gap: 0.5rem;
#tagDropdown {
position: absolute;
- background-color: white;
- box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
z-index: 1000;
max-height: 200px;
overflow-y: scroll;
overflow-x: hidden;
display: flex;
flex-direction: column;
+ backdrop-filter: blur(10px) saturate(150%);
+ border-radius: 10px;
+ }
+
+ :global(body[data-bs-theme='dark']) #tagDropdown {
+ background-color: rgba(87, 87, 87, 0.5);
+ }
+
+ :global(body[data-bs-theme='light']) #tagDropdown {
+ background-color: rgba(196, 196, 196, 0.5);
}
.tag-item {
padding: 5px;
}
+ :global(body[data-bs-theme='dark']) .tag-item.selected {
+ background-color: #5f5f5f;
+ }
+
+ :global(body[data-bs-theme='light']) .tag-item.selected {
+ background-color: #b9b9b9;
+ }
+
.tags {
z-index: 10;
padding: 0.5rem;
margin-bottom: 2rem;
- /* backdrop-filter: blur(8px) saturate(150%);
- background-color: rgba(219, 219, 219, 0.45);
- border: 1px solid #ececec77; */
border-radius: 10px;
- /* color: #ececec; */
}
.loadImageBtn {
transition: all ease 0.2s;
}
- .modal-content {
- backdrop-filter: blur(8px) saturate(150%);
- background-color: rgba(219, 219, 219, 0.45);
+ .modal-header {
+ border-bottom: none;
+ }
+
+ .modal-footer {
+ border-top: none;
}
.files {
- /* margin-right: 2rem; */
margin-bottom: 1rem;
border-radius: 10px;
padding: 1rem;
- /* backdrop-filter: blur(8px) saturate(150%);
- background-color: rgba(219, 219, 219, 0.45);
- border: 1px solid #ececec77; */
}
:global(#uploadIcon) {
}
:global(.TMCommandBar) {
- border-top: 1px solid #ccc;
- border-left: 1px solid #ccc;
- border-right: 1px solid #ccc;
+ border-top: none;
+ border-bottom: none;
height: auto;
flex-wrap: wrap;
+ padding-top: 2px;
+ padding-bottom: 3px;
+ }
+
+ :global(body[data-bs-theme='dark'] .TMCommandBar) {
+ border-left: 1px solid #6a6a6a;
+ border-right: 1px solid #6a6a6a;
+ }
+
+ :global(body[data-bs-theme='light'] .TMCommandBar) {
+ border-left: 1px solid #cccccc;
+ border-right: 1px solid #cccccc;
+ }
+
+ :global(body[data-bs-theme='dark'] .TMCommandBar) {
+ background-color: rgba(70, 70, 70, 0.5);
+ }
+
+ :global(body[data-bs-theme='light'] .TMCommandBar) {
+ background-color: rgba(202, 202, 202, 0.5);
+ }
+
+ :global(body[data-bs-theme='dark'] .TMCommandButton_Inactive) {
+ background-color: transparent;
+ fill: #f0f0f0;
+ }
+
+ :global(body[data-bs-theme='light'] .TMCommandButton_Inactive) {
+ background-color: transparent;
+ fill: #161616;
+ }
+
+ :global(body[data-bs-theme='dark'] .TMCommandButton_Inactive:hover) {
+ background-color: rgba(180, 180, 180, 0.438);
+ }
+
+ :global(body[data-bs-theme='light'] .TMCommandButton_Inactive:hover) {
+ background-color: rgba(180, 180, 180, 0.438);
+ }
+
+ :global(.TMCommandButton) {
+ border-radius: 3px;
+ }
+
+ :global(body[data-bs-theme='dark'] .TinyMDE) {
+ backdrop-filter: blur(8px) saturate(130%);
+ background-color: rgba(50, 50, 50, 0.8);
+ color: #f0f0f0;
+ }
+
+ :global(body[data-bs-theme='light'] .TinyMDE) {
+ backdrop-filter: blur(8px) saturate(130%);
+ background-color: rgba(255, 255, 255, 0.7);
+ color: #1f1f1f;
}
#editor {
import * as bootstrap from 'bootstrap';
import { TolgeeProvider, Tolgee, DevTools, LanguageStorage } from '@tolgee/svelte';
import { FormatIcu } from '@tolgee/format-icu';
+ import { darkMode } from '$lib/settingsStore.js';
const tolgee = Tolgee()
.use(DevTools())
// if on login page, generate neon mesh
if (page.url.pathname === '/login') {
- generateNeonMesh();
+ generateNeonMesh($darkMode);
+ }
+ });
+
+ $effect(() => {
+ if ($darkMode !== undefined) {
+ document.body.setAttribute('data-bs-theme', $darkMode ? 'dark' : 'light');
}
});
background-color: rgba(0, 0, 0, 0.3) !important;
}
- :global(.modal-content) {
+ :global(body[data-bs-theme='dark'] .modal-content) {
backdrop-filter: blur(20px) saturate(150%);
- background-color: rgba(83, 83, 83, 0.85) !important;
+ background-color: rgba(70, 70, 70, 0.5) !important;
border: 1px solid rgba(255, 255, 255, 0.2);
color: #ececec;
}
+ :global(body[data-bs-theme='light'] .modal-content) {
+ backdrop-filter: blur(20px) saturate(150%);
+ background-color: rgba(211, 211, 211, 0.5) !important;
+ border: 1px solid rgba(255, 255, 255, 0.2);
+ color: #161616;
+ }
- :global(.glass) {
+ :global(body[data-bs-theme='dark'] .glass) {
backdrop-filter: blur(14px) saturate(130%);
- background-color: rgba(83, 83, 83, 0.8);
+ background-color: rgba(83, 83, 83, 0.4);
border: 1px solid #62626278;
color: #ececec;
}
+ :global(body[data-bs-theme='light'] .glass) {
+ backdrop-filter: blur(14px) saturate(130%);
+ background-color: rgba(187, 187, 187, 0.3);
+ border: 1px solid #ccc;
+ color: #222;
+ }
- :global(.glassLight) {
- backdrop-filter: blur(8px) saturate(130%);
- background-color: rgba(83, 83, 83, 0.445);
- border: 1px solid #62626278;
- color: #ececec;
+ :global(body[data-bs-theme='dark'] .popover-body > span) {
+ background-color: #444;
}
</style>
<div class="logo-login-flex d-flex justify-content-center align-items-center flex-row h-100">
<div class="logo-wrapper d-flex flex-column align-items-center">
<img id="largeLogo" src={img} alt="locked heart with keyhole" />
- <p>DailyTxT</p>
+ <span class="dailytxt">DailyTxT</span>
</div>
<div class="login-wrapper">
<div class="accordion" id="loginAccordion">
</div>
<style>
+ .dailytxt {
+ margin-top: 1.5rem;
+ color: #f57c00;
+ font-size: 2.7rem;
+ font-weight: 500;
+ line-height: 1rem;
+ position: relative;
+ text-decoration-line: underline;
+ text-decoration-color: #0d6efd;
+ }
+
.language-select-wrapper {
position: absolute;
top: 10px;
#largeLogo {
width: 40%;
min-height: 10%;
+ filter: drop-shadow(0 0 0.7rem #7e7e7e);
+ transition: 0.3s ease;
+ }
+
+ #largeLogo:hover {
+ filter: drop-shadow(0 0 1rem #0d6efd);
+ transform: scale(1.1);
}
.login-wrapper {
width: 70%;
}
+ @media screen and (min-width: 769px) and (max-width: 1000px) {
+ #loginAccordion {
+ width: 80%;
+ }
+ }
+
@media screen and (max-width: 768px) {
.logo-login-flex {
flex-direction: column !important;
min-width: 50%;
max-width: 75%;
}
+
+ #loginAccordion {
+ width: 100%;
+ }
+
+ .logo-wrapper {
+ width: 70%;
+ margin-bottom: 3rem;
+ }
+ }
+
+ @media screen and (max-width: 540px) {
+ .logo-wrapper {
+ width: 90%;
+ }
+
+ .login-wrapper {
+ width: 80%;
+ }
}
</style>
}
.card {
- backdrop-filter: blur(10px);
- background: rgba(255, 255, 255, 0.5);
+ /* backdrop-filter: blur(10px); */
+ /* background: rgba(255, 255, 255, 0.5); */
border: none;
border-radius: 10px;
}