Dark mode and various smaller UI improvements

This commit is contained in:
Ronni Skansing
2025-09-19 12:56:53 +02:00
parent 77ddce72f3
commit b1967c103d
94 changed files with 1857 additions and 703 deletions
+285 -2
View File
@@ -45,9 +45,292 @@
src: url('/Phudu-Black.ttf') format('truetype');
}
/* custom properties for theming */
:root {
/* light mode colors */
--color-bg-primary: #ffffff;
--color-bg-secondary: #f8f9fa;
--color-bg-tertiary: #f1f3f4;
--color-border: #e5e7eb;
--color-border-hover: #d1d5db;
--color-text-primary: #111827;
--color-text-secondary: #6b7280;
--color-text-tertiary: #9ca3af;
--color-shadow: rgba(0, 0, 0, 0.1);
--color-shadow-lg: rgba(0, 0, 0, 0.15);
/* form colors */
--color-input-bg: #ffffff;
--color-input-border: #d1d5db;
--color-input-border-focus: #2563eb;
/* scrollbar colors */
--color-scrollbar-track: #f1f1f1;
--color-scrollbar-thumb: #819efb;
--color-scrollbar-thumb-hover: #6b85d6;
}
.dark {
/* dark mode colors */
--color-bg-primary: #111827;
--color-bg-secondary: #1f2937;
--color-bg-tertiary: #374151;
--color-border: #374151;
--color-border-hover: #4b5563;
--color-text-primary: #f9fafb;
--color-text-secondary: #d1d5db;
--color-text-tertiary: #9ca3af;
--color-shadow: rgba(0, 0, 0, 0.3);
--color-shadow-lg: rgba(0, 0, 0, 0.4);
/* form colors */
--color-input-bg: #374151;
--color-input-border: #4b5563;
--color-input-border-focus: #3b82f6;
/* scrollbar colors */
--color-scrollbar-track: #374151;
--color-scrollbar-thumb: #6b7280;
--color-scrollbar-thumb-hover: #9ca3af;
}
/* global styles */
body {
background-color: var(--color-bg-primary);
color: var(--color-text-primary);
transition:
background-color 0.2s ease,
color 0.2s ease;
}
/* scrollbar styles */
body {
@apply [&::-webkit-scrollbar]:w-2
[&::-webkit-scrollbar-track]:bg-gray-100
[&::-webkit-scrollbar-track]:bg-[var(--color-scrollbar-track)]
[&::-webkit-scrollbar-thumb]:rounded-md
[&::-webkit-scrollbar-thumb]:bg-pc-dusty-light-blue;
[&::-webkit-scrollbar-thumb]:bg-[var(--color-scrollbar-thumb)]
[&::-webkit-scrollbar-thumb:hover]:bg-[var(--color-scrollbar-thumb-hover)];
}
/* dark mode utility classes */
@layer utilities {
.bg-theme-primary {
background-color: var(--color-bg-primary);
}
.bg-theme-secondary {
background-color: var(--color-bg-secondary);
}
.bg-theme-tertiary {
background-color: var(--color-bg-tertiary);
}
.text-theme-primary {
color: var(--color-text-primary);
}
.text-theme-secondary {
color: var(--color-text-secondary);
}
.text-theme-tertiary {
color: var(--color-text-tertiary);
}
.border-theme {
border-color: var(--color-border);
}
.border-theme-hover:hover {
border-color: var(--color-border-hover);
}
.shadow-theme {
box-shadow: 0 1px 3px 0 var(--color-shadow);
}
.shadow-theme-lg {
box-shadow: 0 10px 15px -3px var(--color-shadow-lg);
}
.input-theme {
background-color: var(--color-input-bg);
border-color: var(--color-input-border);
color: var(--color-text-primary);
}
.input-theme:focus {
border-color: var(--color-input-border-focus);
}
}
/* component-specific dark mode overrides */
.dark .bg-gray-50 {
@apply bg-gray-800;
}
.dark .bg-gray-100 {
@apply bg-gray-700;
}
.dark .bg-gray-200 {
@apply bg-gray-600;
}
.dark .bg-white {
@apply bg-gray-800;
}
.dark .text-gray-900 {
@apply text-gray-100;
}
.dark .text-gray-800 {
@apply text-gray-200;
}
.dark .text-gray-700 {
@apply text-gray-300;
}
.dark .text-gray-600 {
@apply text-gray-400;
}
.dark .text-black {
@apply text-white;
}
.dark .border-gray-200 {
@apply border-gray-600;
}
.dark .border-gray-300 {
@apply border-gray-500;
}
/* table dark mode styles */
.dark table {
@apply bg-gray-800;
}
.dark table td {
@apply bg-transparent text-gray-300 border-gray-600;
}
/* form elements dark mode */
.dark input,
.dark textarea,
.dark select {
@apply bg-gray-700 border-gray-600 text-white placeholder-gray-400;
}
.dark input:focus,
.dark textarea:focus,
.dark select:focus {
@apply border-blue-500 ring-blue-500;
}
/* modal and card dark mode */
.dark .modal-content,
.dark .card {
@apply bg-gray-800 border-gray-600;
}
/* button hover effects in dark mode */
.dark .bg-cta-blue:hover {
@apply bg-blue-600;
}
.dark .bg-pc-darkblue:hover {
@apply bg-blue-900;
}
/* custom scrollbar for dark mode containers */
.dark .overflow-auto,
.dark .overflow-x-auto,
.dark .overflow-y-auto {
scrollbar-width: thin;
scrollbar-color: #6b7280 #374151;
}
.dark .overflow-auto::-webkit-scrollbar,
.dark .overflow-x-auto::-webkit-scrollbar,
.dark .overflow-y-auto::-webkit-scrollbar {
width: 8px;
height: 8px;
}
.dark .overflow-auto::-webkit-scrollbar-track,
.dark .overflow-x-auto::-webkit-scrollbar-track,
.dark .overflow-y-auto::-webkit-scrollbar-track {
background: #374151;
}
.dark .overflow-auto::-webkit-scrollbar-thumb,
.dark .overflow-x-auto::-webkit-scrollbar-thumb,
.dark .overflow-y-auto::-webkit-scrollbar-thumb {
background: #6b7280;
border-radius: 4px;
}
.dark .overflow-auto::-webkit-scrollbar-thumb:hover,
.dark .overflow-x-auto::-webkit-scrollbar-thumb:hover,
.dark .overflow-y-auto::-webkit-scrollbar-thumb:hover {
background: #9ca3af;
}
/* native date picker dark mode styling */
.dark input[type='date']::-webkit-calendar-picker-indicator,
.dark input[type='time']::-webkit-calendar-picker-indicator,
.dark input[type='datetime-local']::-webkit-calendar-picker-indicator {
filter: invert(1);
}
.dark input[type='date']::-webkit-datetime-edit,
.dark input[type='time']::-webkit-datetime-edit,
.dark input[type='datetime-local']::-webkit-datetime-edit {
color: rgb(209 213 219);
}
.dark input[type='date']::-webkit-datetime-edit-fields-wrapper,
.dark input[type='time']::-webkit-datetime-edit-fields-wrapper,
.dark input[type='datetime-local']::-webkit-datetime-edit-fields-wrapper {
background: rgb(55 65 81);
}
/* force date picker to use system dark theme */
.dark input[type='date'],
.dark input[type='time'],
.dark input[type='datetime-local'] {
color-scheme: dark;
}
/* selected date background in dark mode */
.dark input[type='date']::-webkit-datetime-edit-day-field:focus,
.dark input[type='date']::-webkit-datetime-edit-month-field:focus,
.dark input[type='date']::-webkit-datetime-edit-year-field:focus,
.dark input[type='time']::-webkit-datetime-edit-hour-field:focus,
.dark input[type='time']::-webkit-datetime-edit-minute-field:focus {
background-color: rgb(55 65 81);
color: rgb(209 213 219);
}
/* transitions for smooth theme switching */
*,
*::before,
*::after {
transition:
background-color 0.2s ease,
border-color 0.2s ease,
color 0.2s ease;
}
/* override transitions for elements that shouldn't animate */
.no-transition,
.no-transition *,
.no-transition *::before,
.no-transition *::after {
transition: none !important;
}
+11 -6
View File
@@ -243,7 +243,9 @@
</script>
{#if visible}
<div class="fixed top-0 left-0 w-full h-full bg-cta-blue opacity-20 blur-xl" />
<div
class="fixed top-0 left-0 w-full h-full bg-cta-blue dark:bg-gray-900 opacity-20 blur-xl transition-colors duration-200"
/>
<div
class="fixed top-0 left-0 w-full h-full flex justify-center items-center backdrop-blur-sm z-20"
role="dialog"
@@ -253,11 +255,11 @@
>
<section
bind:this={alertElement}
class="shadow-xl w-[32rem] bg-white opacity-100 rounded-md flex flex-col"
class="shadow-xl dark:shadow-gray-900/70 w-[32rem] bg-white dark:bg-gray-800 opacity-100 rounded-md flex flex-col transition-colors duration-200"
>
<!-- Header -->
<div
class="bg-red-700 text-white rounded-t-md py-4 px-8 flex items-center justify-between flex-shrink-0"
class="bg-red-700 dark:bg-red-800 text-white rounded-t-md py-4 px-8 flex items-center justify-between flex-shrink-0 transition-colors duration-200"
>
<div class="flex items-center">
<svg
@@ -289,7 +291,10 @@
<!-- Content -->
<div class="px-8 py-6">
<div id="alert-description" class="text-gray-600">
<div
id="alert-description"
class="text-gray-600 dark:text-gray-300 transition-colors duration-200"
>
{#if $$slots.default}
<slot />
{:else}
@@ -312,13 +317,13 @@
<!-- Footer -->
<div
class="py-4 row-span-2 col-start-1 col-span-3 border-t-2 w-full flex flex-row justify-center items-center sm:justify-center md:justify-center lg:justify-end xl:justify-end 2xl:justify-end px-8 bg-gray-50 rounded-b-md"
class="py-4 row-span-2 col-start-1 col-span-3 border-t-2 border-gray-200 dark:border-gray-600 w-full flex flex-row justify-center items-center sm:justify-center md:justify-center lg:justify-end xl:justify-end 2xl:justify-end px-8 bg-gray-50 dark:bg-gray-700 rounded-b-md transition-colors duration-200"
>
{#if !noCancel}
<button
type="reset"
on:click={close}
class="bg-slate-400 hover:bg-slate-300 text-sm mr-2 uppercase font-bold px-4 py-2 text-white rounded-md"
class="bg-slate-400 hover:bg-slate-300 dark:bg-gray-600 dark:hover:bg-gray-500 text-sm mr-2 uppercase font-bold px-4 py-2 text-white rounded-md transition-colors duration-200"
>
{cancel}
</button>
+15 -10
View File
@@ -66,15 +66,20 @@
});
</script>
<div class="relative h-2">
<TextFieldSelect
id="autoRefresh"
value={$autoRefreshStore.enabled
? options.byValue($autoRefreshStore.interval.toString())
: 'Disabled'}
onSelect={handleIntervalChange}
options={options.keys()}
inline={true}
size={'small'}>Auto-Refresh</TextFieldSelect
<div class="flex items-center gap-2">
<span class="font-semibold text-slate-600 dark:text-gray-300 py-2 transition-colors duration-200"
>Auto-Refresh</span
>
<div class="relative">
<TextFieldSelect
id="autoRefresh"
value={$autoRefreshStore.enabled
? options.byValue($autoRefreshStore.interval.toString())
: 'Disabled'}
onSelect={handleIntervalChange}
options={options.keys()}
inline={true}
size={'small'}
></TextFieldSelect>
</div>
</div>
+2 -1
View File
@@ -6,9 +6,10 @@
<button
{disabled}
class="self-start mt-6 bg-gradient-to-b from-blue-500 to-indigo-400 px-4 w-56 py-2 hover:from-blue-400 hover:to-indigo-400 text-white font-bold uppercase rounded-md mb-10"
class="self-start mt-6 bg-gradient-to-b from-blue-500 to-indigo-400 dark:from-blue-600 dark:to-indigo-500 px-4 w-56 py-2 hover:from-blue-400 hover:to-indigo-400 dark:hover:from-blue-500 dark:hover:to-indigo-400 text-white font-bold uppercase rounded-md mb-10 transition-all duration-200"
on:click
class:opacity-50={disabled}
class:dark:opacity-60={disabled}
class:cursor-not-allowed={disabled}
{type}
>
+3 -1
View File
@@ -9,10 +9,12 @@
</script>
<button
class="{backgroundColor} px-6 py-2 text-white rounded-md uppercase font-semibold hover:opacity-80 text-sm {css}"
class="{backgroundColor} px-6 py-2 text-white rounded-md uppercase font-semibold hover:opacity-80 text-sm transition-all duration-200 dark:hover:opacity-90 {css}"
class:w-32={size === 'small'}
class:w-36={size === 'medium'}
class:w-60={size === 'large'}
class:disabled:opacity-50={disabled}
class:dark:disabled:opacity-60={disabled}
{disabled}
on:click
>
+2 -2
View File
@@ -2,7 +2,7 @@
export let disabled = false;
/**
* @type {"submit" | "button" | "reset"}
*/
export let type = 'submit';
</script>
@@ -11,7 +11,7 @@
<button
{disabled}
{type}
class="w-full p-2 text-white text-2xl bg-cta-blue rounded-lg font-phudu font-bold"
class="w-full p-2 text-white text-2xl bg-cta-blue dark:bg-blue-600 hover:bg-blue-700 dark:hover:bg-blue-700 rounded-lg font-phudu font-bold transition-colors duration-200 disabled:opacity-50 dark:disabled:opacity-60"
>Login</button
>
</div>
@@ -41,11 +41,12 @@
let isGeneratingCalendar = false;
// Use consistent colors that match the design system - these work well in both light and dark modes
const COLORS = {
SCHEDULED: '#62aded',
ACTIVE: '#5557f6',
COMPLETED: '#69e1ab',
SELF_MANAGED: '#9F7AEA'
SCHEDULED: '#62aded', // campaign-scheduled
ACTIVE: '#5557f6', // campaign-active
COMPLETED: '#4cb5b5', // message-read - much more muted than bright green
SELF_MANAGED: '#9F7AEA' // purple
};
function sortCampaignsByPriority(campaigns, day) {
@@ -306,18 +307,20 @@
}
</script>
<div class="w-full bg-white rounded-lg shadow-sm p-4 border border-gray-200">
<div
class="w-full bg-white dark:bg-gray-800 rounded-lg shadow-sm p-4 border border-gray-200 dark:border-gray-700 transition-colors duration-200"
>
<div class="space-y-4 max-w-5xl mx-auto min-h-[600px]">
<!-- Navigation Controls -->
<div class="flex justify-center items-center">
<button
class="p-2 rounded hover:bg-gray-100 mx-4"
class="p-2 rounded hover:bg-gray-100 dark:hover:bg-gray-700 mx-4 transition-colors duration-200"
on:click={previousMonth}
disabled={isLoadingNewMonth}
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-5 w-5 text-gray-600"
class="h-5 w-5 text-gray-600 dark:text-gray-300"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
@@ -331,18 +334,18 @@
</svg>
</button>
<h2 class="text-lg font-semibold">
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">
{format(currentMonth, 'MMMM yyyy')}
</h2>
<button
class="p-2 rounded hover:bg-gray-100 mx-4"
class="p-2 rounded hover:bg-gray-100 dark:hover:bg-gray-700 mx-4 transition-colors duration-200"
on:click={nextMonth}
disabled={isLoadingNewMonth}
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-5 w-5 text-gray-600"
class="h-5 w-5 text-gray-600 dark:text-gray-300"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
@@ -356,7 +359,7 @@
<div class="flex justify-center flex-wrap gap-4 text-sm">
{#each [{ key: 'SCHEDULED', color: COLORS.SCHEDULED, label: 'Scheduled' }, { key: 'ACTIVE', color: COLORS.ACTIVE, label: 'Active' }, { key: 'COMPLETED', color: COLORS.COMPLETED, label: 'Completed' }, { key: 'SELF_MANAGED', color: COLORS.SELF_MANAGED, label: 'Self-managed' }] as item}
<button
class="flex items-center cursor-pointer select-none"
class="flex items-center cursor-pointer select-none hover:opacity-80 transition-opacity duration-200"
on:click={() => toggleFilter(item.key)}
>
<div
@@ -364,7 +367,7 @@
style="background-color: {item.color}; opacity: {activeFilters[item.key] ? '1' : '0.3'}"
></div>
<span
class="transition-opacity duration-200"
class="transition-opacity duration-200 text-gray-700 dark:text-gray-300"
style="opacity: {activeFilters[item.key] ? '1' : '0.5'}">{item.label}</span
>
</button>
@@ -376,7 +379,7 @@
<!-- Day headers -->
<div class="grid grid-cols-7 text-center mb-1">
{#each ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'] as day}
<div class="text-sm font-medium text-gray-600">{day}</div>
<div class="text-sm font-medium text-gray-600 dark:text-gray-300">{day}</div>
{/each}
</div>
@@ -384,8 +387,10 @@
{#if !isInitialized || isGeneratingCalendar || isLoadingNewMonth}
<div class="min-h-[450px] flex items-center justify-center">
<div class="flex items-center space-x-2">
<div class="animate-spin rounded-full h-6 w-6 border-b-2 border-blue-600"></div>
<span class="text-gray-600">Loading calendar...</span>
<div
class="animate-spin rounded-full h-6 w-6 border-b-2 border-blue-600 dark:border-blue-400"
></div>
<span class="text-gray-600 dark:text-gray-300">Loading calendar...</span>
</div>
</div>
{:else}
@@ -395,17 +400,19 @@
{#each week as day}
<div
class="calendar-day relative border rounded-md overflow-hidden {scrollBarClassesHorizontal} {day.isToday
? 'border-gray-700 bg-gray-50'
: 'border-gray-200'}
{day.isCurrentMonth ? 'bg-white' : 'bg-gray-50'}"
? 'border-gray-700 dark:border-gray-400 bg-gray-50 dark:bg-gray-700'
: 'border-gray-200 dark:border-gray-600'}
{day.isCurrentMonth
? 'bg-white dark:bg-gray-800'
: 'bg-gray-50 dark:bg-gray-700'} transition-colors duration-200"
>
<!-- Date number -->
<div
class="text-sm p-1 {day.isToday
? 'font-bold text-gray-800'
? 'font-bold text-gray-800 dark:text-white'
: day.isCurrentMonth
? 'text-gray-600'
: 'text-gray-400'}"
? 'text-gray-600 dark:text-gray-300'
: 'text-gray-400 dark:text-gray-500'}"
>
{day.date.getDate()}
</div>
@@ -60,7 +60,7 @@
let chartContainer;
let sizingContainer;
let width = 300;
let height = 200; // Balanced height to prevent overflow
let height = 240; // Increased height for better label spacing
let containerReady = false;
// User controls for N
@@ -96,7 +96,7 @@
$: margin = {
top: 15,
right: Math.min(180, Math.max(160, width * 0.28)), // 28% of width, min 160px, max 180px
bottom: 30,
bottom: 50, // Increased bottom margin for better label spacing
left: 50
};
@@ -344,17 +344,26 @@
const stop1 = document.createElementNS('http://www.w3.org/2000/svg', 'stop');
stop1.setAttribute('offset', '0%');
stop1.setAttribute('stop-color', '#f8f9fa');
stop1.setAttribute(
'stop-color',
document.documentElement.classList.contains('dark') ? '#374151' : '#f8f9fa'
);
stop1.setAttribute('stop-opacity', '0.3');
const stop2 = document.createElementNS('http://www.w3.org/2000/svg', 'stop');
stop2.setAttribute('offset', '50%');
stop2.setAttribute('stop-color', '#e9ecef');
stop2.setAttribute(
'stop-color',
document.documentElement.classList.contains('dark') ? '#4b5563' : '#e9ecef'
);
stop2.setAttribute('stop-opacity', '0.5');
const stop3 = document.createElementNS('http://www.w3.org/2000/svg', 'stop');
stop3.setAttribute('offset', '100%');
stop3.setAttribute('stop-color', '#f8f9fa');
stop3.setAttribute(
'stop-color',
document.documentElement.classList.contains('dark') ? '#374151' : '#f8f9fa'
);
stop3.setAttribute('stop-opacity', '0.3');
gradient.appendChild(stop1);
@@ -372,7 +381,10 @@
line.setAttribute('x2', (width - margin.right).toString());
line.setAttribute('y1', y.toString());
line.setAttribute('y2', y.toString());
line.setAttribute('stroke', '#E5E7EB');
line.setAttribute(
'stroke',
document.documentElement.classList.contains('dark') ? '#4b5563' : '#E5E7EB'
);
line.setAttribute('stroke-width', '1');
line.setAttribute('opacity', '0.4');
svg.appendChild(line);
@@ -386,7 +398,10 @@
vLine.setAttribute('x2', x.toString());
vLine.setAttribute('y1', margin.top.toString());
vLine.setAttribute('y2', (height - margin.bottom).toString());
vLine.setAttribute('stroke', '#e5e7eb');
vLine.setAttribute(
'stroke',
document.documentElement.classList.contains('dark') ? '#4b5563' : '#e5e7eb'
);
vLine.setAttribute('stroke-width', '0.5');
vLine.setAttribute('opacity', '0.3');
svg.appendChild(vLine);
@@ -399,7 +414,10 @@
yAxis.setAttribute('x2', margin.left.toString());
yAxis.setAttribute('y1', margin.top.toString());
yAxis.setAttribute('y2', (height - margin.bottom).toString());
yAxis.setAttribute('stroke', '#6B7280');
yAxis.setAttribute(
'stroke',
document.documentElement.classList.contains('dark') ? '#9ca3af' : '#6B7280'
);
yAxis.setAttribute('stroke-width', '2');
svg.appendChild(yAxis);
@@ -408,7 +426,10 @@
xAxis.setAttribute('x2', (width - margin.right).toString());
xAxis.setAttribute('y1', (height - margin.bottom).toString());
xAxis.setAttribute('y2', (height - margin.bottom).toString());
xAxis.setAttribute('stroke', '#6B7280');
xAxis.setAttribute(
'stroke',
document.documentElement.classList.contains('dark') ? '#9ca3af' : '#6B7280'
);
xAxis.setAttribute('stroke-width', '2');
svg.appendChild(xAxis);
@@ -420,7 +441,10 @@
text.setAttribute('y', (y - 2).toString());
text.setAttribute('text-anchor', 'end');
text.setAttribute('font-size', '11');
text.setAttribute('fill', '#6B7280');
text.setAttribute(
'fill',
document.documentElement.classList.contains('dark') ? '#d1d5db' : '#6B7280'
);
text.setAttribute('font-weight', '500');
text.setAttribute('alignment-baseline', 'middle');
text.textContent = `${value}%`;
@@ -432,10 +456,13 @@
const x = xScale(i);
const text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
text.setAttribute('x', x.toString());
text.setAttribute('y', (height - margin.bottom + 18).toString());
text.setAttribute('y', (height - margin.bottom + 25).toString());
text.setAttribute('text-anchor', 'middle');
text.setAttribute('font-size', '11');
text.setAttribute('fill', '#000');
text.setAttribute(
'fill',
document.documentElement.classList.contains('dark') ? '#f9fafb' : '#000'
);
text.setAttribute('alignment-baseline', 'middle');
// Show date (YYYY/MM) and campaign name on the same line
const labelSpan = document.createElementNS('http://www.w3.org/2000/svg', 'tspan');
@@ -534,9 +561,9 @@
}
function createLegend(svg, svgRoot) {
const legendY = margin.top - 5; // Move legend higher
const legendY = margin.top + 5; // Align with chart top area
const legendX = width - margin.right + 10;
const legendSpacing = 12; // Reduce spacing between legend items
const legendSpacing = 18; // More spacing between legend items
// Build a flat list of legend items (main and moving averages)
const legendItems = [];
@@ -684,8 +711,14 @@
const tooltipRect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
tooltipRect.setAttribute('rx', '4');
tooltipRect.setAttribute('fill', '#1F2937');
tooltipRect.setAttribute('stroke', '#374151');
tooltipRect.setAttribute(
'fill',
document.documentElement.classList.contains('dark') ? '#111827' : '#1F2937'
);
tooltipRect.setAttribute(
'stroke',
document.documentElement.classList.contains('dark') ? '#374151' : '#4b5563'
);
tooltipRect.setAttribute('opacity', '0.95');
const tooltipText = document.createElementNS('http://www.w3.org/2000/svg', 'text');
@@ -829,18 +862,26 @@
<div>
{#if debouncedIsLoading}
<div class="flex items-center justify-center h-64">
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
<span class="ml-2 text-gray-600">Loading trend data...</span>
<div
class="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 dark:border-blue-400"
></div>
<span class="ml-2 text-gray-600 dark:text-gray-300 transition-colors duration-200"
>Loading trend data...</span
>
</div>
{:else if !hasAttemptedLoad && debouncedShowPending}
<div class="flex items-center justify-center h-64">
<span class="text-gray-400 text-sm">Preparing trend data...</span>
<span class="text-gray-400 dark:text-gray-500 text-sm transition-colors duration-200"
>Preparing trend data...</span
>
</div>
{:else if hasAttemptedLoad && !isLoading && !debouncedIsLoading && chartData.length === 0}
<div class="flex items-center justify-center h-64 bg-gray-50 rounded-lg">
<div
class="flex items-center justify-center h-64 bg-gray-50 dark:bg-gray-800 rounded-lg transition-colors duration-200"
>
<div class="text-center">
<svg
class="mx-auto h-12 w-12 text-gray-400"
class="mx-auto h-12 w-12 text-gray-400 dark:text-gray-500 transition-colors duration-200"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
@@ -852,17 +893,23 @@
d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"
/>
</svg>
<h3 class="mt-2 text-sm font-medium text-gray-900">No campaign data</h3>
<p class="mt-1 text-sm text-gray-500">
<h3
class="mt-2 text-sm font-medium text-gray-900 dark:text-gray-200 transition-colors duration-200"
>
No campaign data
</h3>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400 transition-colors duration-200">
Campaign statistics will appear here once campaigns are completed.
</p>
</div>
</div>
{:else if hasAttemptedLoad && !isLoading && !debouncedIsLoading && chartData.length === 1}
<div class="bg-blue-50 border border-blue-200 rounded-lg p-6">
<div
class="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-6 transition-colors duration-200"
>
<div class="flex items-center">
<svg
class="h-6 w-6 text-blue-600 mr-2"
class="h-6 w-6 text-blue-600 dark:text-blue-400 mr-2 transition-colors duration-200"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
@@ -875,44 +922,78 @@
/>
</svg>
<div>
<h4 class="text-sm font-medium text-blue-900">Single Campaign Data</h4>
<p class="text-sm text-blue-700">
<h4
class="text-sm font-medium text-blue-900 dark:text-blue-200 transition-colors duration-200"
>
Single Campaign Data
</h4>
<p class="text-sm text-blue-700 dark:text-blue-300 transition-colors duration-200">
Trends will appear when you have 2 or more completed campaigns.
</p>
</div>
</div>
<div class="grid grid-cols-4 gap-4">
<div class="text-center">
<div class="text-2xl font-bold text-blue-600">{chartData[0].openRate}%</div>
<div class="text-sm text-gray-600">Open Rate</div>
<div
class="text-2xl font-bold text-blue-600 dark:text-blue-400 transition-colors duration-200"
>
{chartData[0].openRate}%
</div>
<div class="text-sm text-gray-600 dark:text-gray-400 transition-colors duration-200">
Open Rate
</div>
</div>
<div class="text-center">
<div class="text-2xl font-bold text-green-600">{chartData[0].clickRate}%</div>
<div class="text-sm text-gray-600">Click Rate</div>
<div
class="text-2xl font-bold text-green-600 dark:text-green-400 transition-colors duration-200"
>
{chartData[0].clickRate}%
</div>
<div class="text-sm text-gray-600 dark:text-gray-400 transition-colors duration-200">
Click Rate
</div>
</div>
<div class="text-center">
<div class="text-2xl font-bold text-yellow-600">{chartData[0].submissionRate}%</div>
<div class="text-sm text-gray-600">Submission Rate</div>
<div
class="text-2xl font-bold text-yellow-600 dark:text-yellow-400 transition-colors duration-200"
>
{chartData[0].submissionRate}%
</div>
<div class="text-sm text-gray-600 dark:text-gray-400 transition-colors duration-200">
Submission Rate
</div>
</div>
<div class="text-center">
<div class="text-2xl font-bold text-indigo-600">{chartData[0].reportRate}%</div>
<div class="text-sm text-gray-600">Report Rate</div>
<div
class="text-2xl font-bold text-indigo-600 dark:text-indigo-400 transition-colors duration-200"
>
{chartData[0].reportRate}%
</div>
<div class="text-sm text-gray-600 dark:text-gray-400 transition-colors duration-200">
Report Rate
</div>
</div>
</div>
</div>
{:else if hasAttemptedLoad && !isLoading && !debouncedIsLoading && chartData.length >= 2}
<div class="bg-white rounded-lg border border-gray-200 p-6">
<div
class="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-600 p-6 transition-colors duration-200"
>
<!-- Trendline stats and controls above chart -->
<div class="flex flex-row items-center justify-between mb-2 pb-0 flex-wrap gap-2">
<h4 class="text-sm font-medium text-gray-600 m-0">
<h4
class="text-sm font-medium text-gray-600 dark:text-gray-300 m-0 transition-colors duration-200"
>
Trendline: Last {trendStats ? trendStats.n : chartData.length} Campaigns (average)
</h4>
<div class="flex flex-wrap items-center gap-2 mb-0">
<label class="flex items-center gap-1 text-xs text-gray-700">
<label
class="flex items-center gap-1 text-xs text-gray-700 dark:text-gray-300 transition-colors duration-200"
>
Time range:
<select
bind:value={selectedTimeRange}
class="border rounded px-1 py-0 text-xs"
class="border border-gray-300 dark:border-gray-600 rounded px-1 py-0 text-xs bg-white dark:bg-gray-700 text-gray-700 dark:text-gray-200 transition-colors duration-200"
style="height: 1.5rem;"
>
{#each timeRanges as range}
@@ -921,26 +1002,30 @@
</select>
</label>
{#if chartData.length > 1}
<label class="flex items-center gap-1 text-xs text-gray-700">
<label
class="flex items-center gap-1 text-xs text-gray-700 dark:text-gray-300 transition-colors duration-200"
>
Trendline N:
<input
type="number"
min="1"
max={chartData.length}
bind:value={trendN}
class="border rounded px-1 py-0 w-10 text-xs"
class="border border-gray-300 dark:border-gray-600 rounded px-1 py-0 w-10 text-xs bg-white dark:bg-gray-700 text-gray-700 dark:text-gray-200 transition-colors duration-200"
style="height: 1.5rem;"
/>
</label>
{/if}
<label class="flex items-center gap-1 text-xs text-gray-700">
<label
class="flex items-center gap-1 text-xs text-gray-700 dark:text-gray-300 transition-colors duration-200"
>
Moving Avg N:
<input
type="number"
min="2"
max={chartData.length}
bind:value={movingAvgN}
class="border rounded px-1 py-0 w-10 text-xs"
class="border border-gray-300 dark:border-gray-600 rounded px-1 py-0 w-10 text-xs bg-white dark:bg-gray-700 text-gray-700 dark:text-gray-200 transition-colors duration-200"
style="height: 1.5rem;"
/>
</label>
@@ -956,7 +1041,10 @@
class="w-3 h-3 rounded-full mr-2"
style="background-color: {metric.color}"
></div>
<span class="text-sm font-medium text-gray-700">{metric.label}</span>
<span
class="text-sm font-medium text-gray-700 dark:text-gray-300 transition-colors duration-200"
>{metric.label}</span
>
</div>
<div class="mt-1">
<span class="text-2xl font-bold" style="color: {metric.color}">
@@ -971,7 +1059,9 @@
{/each}
</div>
{:else}
<div class="text-center text-gray-400 text-sm py-4">
<div
class="text-center text-gray-400 dark:text-gray-500 text-sm py-4 transition-colors duration-200"
>
No trendline stats to display (trendStats is null or not enough data).
</div>
{/if}
@@ -979,7 +1069,7 @@
{#if containerReady}
<div
bind:this={chartContainer}
class="min-h-[180px] max-h-[220px] w-full box-border relative rounded-md bg-white m-1"
class="min-h-[220px] max-h-[280px] w-full box-border relative rounded-md bg-white dark:bg-gray-800 m-1 transition-colors duration-200"
style="contain: layout style;"
></div>
{/if}
@@ -1038,6 +1128,10 @@
background: white;
border-radius: 8px;
padding: 8px;
transition: background-color 0.2s ease;
}
:global(.dark .campaign-trend-chart) {
background: #1f2937 !important;
}
:global(.line-enhanced) {
stroke-width: 5 !important;
@@ -46,7 +46,7 @@
class:items-center={inline}
>
<div class="flex items-center">
<p class="font-semibold text-slate-600 py-2">
<p class="font-semibold text-slate-600 dark:text-gray-300 py-2 transition-colors duration-200">
<slot />
</p>
{#if toolTipText.length > 0}
@@ -55,8 +55,10 @@
</ToolTip>
{/if}
{#if optional === true}
<div class="bg-gray-100 ml-2 px-2 rounded-md">
<p class="text-slate-600 text-xs">optional</p>
<div class="bg-gray-100 dark:bg-gray-700 ml-2 px-2 rounded-md transition-colors duration-200">
<p class="text-slate-600 dark:text-gray-300 text-xs transition-colors duration-200">
optional
</p>
</div>
{/if}
</div>
@@ -71,12 +73,12 @@
on:change
/>
<div
class="w-5 h-5 border-2 border-slate-300 rounded
peer-checked:border-cta-blue peer-checked:bg-cta-blue
class="w-5 h-5 border-2 border-slate-300 dark:border-gray-600 rounded
peer-checked:border-cta-blue dark:peer-checked:border-blue-500 peer-checked:bg-cta-blue dark:peer-checked:bg-blue-500
transition-all duration-200 ease-in-out
flex items-center justify-center
bg-slate-50
focus-within:ring-2 focus-within:ring-cta-blue focus-within:ring-offset-2"
bg-slate-50 dark:bg-gray-700
focus-within:ring-2 focus-within:ring-cta-blue dark:focus-within:ring-blue-500 focus-within:ring-offset-2 dark:focus-within:ring-offset-gray-800"
>
{#if value}
<svg class="w-3 h-3 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -160,7 +160,9 @@
</script>
{#if visible}
<div class="fixed top-0 left-0 w-full h-full bg-cta-blue opacity-20 blur-xl" />
<div
class="fixed top-0 left-0 w-full h-full bg-cta-blue dark:bg-gray-900 opacity-20 blur-xl transition-colors duration-200"
/>
<div
class="fixed top-0 left-0 w-full h-full flex justify-center items-center backdrop-blur-sm z-20"
role="dialog"
@@ -170,23 +172,34 @@
>
<section
bind:this={confirmElement}
class="flex flex-col items-center w-1/3 bg-slate-100 shadow-xl rounded-md"
class="flex flex-col items-center w-1/3 bg-slate-100 dark:bg-gray-800 shadow-xl dark:shadow-gray-900/70 rounded-md transition-colors duration-200"
>
<div class="bg-cta-orange2 text-white rounded-tl-md rounded-tr-md w-full">
<div
class="bg-cta-orange2 dark:bg-orange-600 text-white rounded-tl-md rounded-tr-md w-full transition-colors duration-200"
>
<p id="confirm-title" class="uppercase font-bold text-center py">Confirm action</p>
</div>
<h1 class="uppercase font-bold text-center text-gray-500 text-4xl pt-10">Are you sure?</h1>
<p id="confirm-description" class="text-center">{confirm_text}</p>
<h1
class="uppercase font-bold text-center text-gray-500 dark:text-gray-300 text-4xl pt-10 transition-colors duration-200"
>
Are you sure?
</h1>
<p
id="confirm-description"
class="text-center text-gray-700 dark:text-gray-200 transition-colors duration-200"
>
{confirm_text}
</p>
<div class="flex pt-4 pb-8">
<button
class="mt-6 bg-grayblue-dark w-40 py-2 mr-2 hover:bg-slate-300 text-white font-bold uppercase rounded-md"
class="mt-6 bg-grayblue-dark dark:bg-gray-600 w-40 py-2 mr-2 hover:bg-slate-300 dark:hover:bg-gray-500 text-white font-bold uppercase rounded-md transition-colors duration-200"
on:click={close}
aria-label="Cancel action"
>
no
</button>
<button
class="mt-6 bg-cta-blue w-56 py-2 hover:bg-blue-500 text-white font-bold uppercase rounded-md"
class="mt-6 bg-cta-blue dark:bg-blue-600 w-56 py-2 hover:bg-blue-500 dark:hover:bg-blue-700 text-white font-bold uppercase rounded-md transition-colors duration-200"
on:click={confirm}
aria-label="Confirm action"
>
+10 -4
View File
@@ -59,7 +59,9 @@
class:w-60={labelWidth === 'large'}
>
<div class="flex items-center">
<p class="font-semibold text-slate-600 py-2">
<p
class="font-semibold text-slate-600 dark:text-gray-300 py-2 transition-colors duration-200"
>
<slot />
</p>
{#if toolTipText.length > 0}
@@ -68,8 +70,12 @@
</ToolTip>
{/if}
{#if optional === true}
<div class="bg-gray-100 ml-2 px-2 rounded-md">
<p class="text-slate-600 text-xs">optional</p>
<div
class="bg-gray-100 dark:bg-gray-700 ml-2 px-2 rounded-md transition-colors duration-200"
>
<p class="text-slate-600 dark:text-gray-300 text-xs transition-colors duration-200">
optional
</p>
</div>
{/if}
</div>
@@ -86,7 +92,7 @@
{required}
{disabled}
autocomplete="off"
class="rounded-md text-center py-2 pl-2 text-gray-600 border border-transparent focus:outline-none focus:border-solid focus:border-slate-400 focus:bg-gray-100 bg-grayblue-light font-normal"
class="rounded-md text-center py-2 pl-2 text-gray-600 dark:text-gray-200 border border-transparent dark:border-gray-600 focus:outline-none focus:border-solid focus:border-slate-400 dark:focus:border-blue-500 focus:bg-gray-100 dark:focus:bg-gray-600 bg-grayblue-light dark:bg-gray-700 font-normal transition-colors duration-200"
class:text-left={textAlign == 'left'}
class:text-center={textAlign == 'center'}
class:text-right={textAlign == 'right'}
@@ -112,7 +112,7 @@
class:w-60={labelWidth === 'large'}
>
<div class="flex items-center">
<p class="font-semibold text-slate-600 py-2">
<p class="font-semibold text-slate-600 dark:text-gray-300 py-2 transition-colors duration-200">
<slot />
</p>
{#if toolTipText.length > 0}
@@ -121,8 +121,8 @@
</ToolTip>
{/if}
{#if optional === true}
<div class="bg-gray-100 ml-2 px-2 rounded-md">
<p class="text-slate-600 text-xs">optional</p>
<div class="bg-gray-100 dark:bg-gray-700 ml-2 px-2 rounded-md transition-colors duration-200">
<p class="text-slate-600 dark:text-gray-300 text-xs">optional</p>
</div>
{/if}
</div>
@@ -139,7 +139,7 @@
{readonly}
{required}
autocomplete="off"
class="w-44 rounded-md py-2 pl-2 text-gray-600 border border-transparent focus:outline-none focus:border-solid focus:border-slate-400 focus:bg-gray-100 bg-grayblue-light font-normal"
class="w-44 rounded-md py-2 pl-2 text-gray-600 dark:text-gray-300 border border-transparent focus:outline-none focus:border-solid focus:border-slate-400 dark:focus:border-gray-500 focus:bg-gray-100 dark:focus:bg-gray-600 bg-grayblue-light dark:bg-gray-700 font-normal transition-colors duration-200"
class:opacity-90={readonly}
/>
<input
@@ -153,7 +153,8 @@
{readonly}
{required}
autocomplete="off"
class="ml-2 rounded-md py-2 pl-2 text-gray-600 text-center border border-transparent focus:outline-none focus:border-solid focus:border-slate-400 focus:bg-gray-100 bg-grayblue-light font-normal"
class="ml-2 rounded-md py-2 pl-2 text-gray-600 dark:text-gray-300 text-center border border-transparent focus:outline-none focus:border-solid focus:border-slate-400 dark:focus:border-gray-500 focus:bg-gray-100 dark:focus:bg-gray-600 bg-grayblue-light dark:bg-gray-700 font-normal transition-colors duration-200"
class:bg-yellow-200={readonly}
class:dark:bg-yellow-700={readonly}
/>
</div>
+1 -1
View File
@@ -14,7 +14,7 @@
}
</script>
<div>
<div class="text-gray-900 dark:text-gray-100 transition-colors duration-200">
{#if date}
{#if !hideHours}
{date.toLocaleString()}
@@ -28,7 +28,7 @@
<div>
<div>
<button
class="fixed border-2 border-slate-500 bg-white w-8 h-8 left-2 pb-1 bottom-4 text-white rounded-full hover:right-3 hover:bottom-3 hover:w-10 hover:h-10 transition-all"
class="fixed border-2 border-slate-500 dark:border-slate-400 bg-white dark:bg-gray-700 w-8 h-8 left-2 pb-1 bottom-4 text-white rounded-full hover:right-3 hover:bottom-3 hover:w-10 hover:h-10 transition-all"
on:click={() => (visible = !visible)}
>
{#if visible}
@@ -41,46 +41,75 @@
{#if visible}
<div
transition:fade={{ duration: 100 }}
class="absolute right-0 h-auto bg-black text-white p-4 z-40"
class="absolute right-0 h-auto bg-black dark:bg-gray-900 text-white p-4 z-40 border border-gray-600 dark:border-gray-500"
>
<h1 class="text-xl m-4">Developer Panel</h1>
<h2 class="text-lg font-bold m-4">Links</h2>
<ul class="m-4">
<li>
<a href="http://localhost:8101" target="_blank">Database</a>
<a
href="http://localhost:8101"
target="_blank"
class="text-blue-400 hover:text-blue-300 underline">Database</a
>
</li>
<li>
<a href="http://localhost:8102" target="_blank">Mailbox</a>
<a
href="http://localhost:8102"
target="_blank"
class="text-blue-400 hover:text-blue-300 underline">Mailbox</a
>
</li>
<li>
<a href="http://localhost:8103" target="_blank">Container logs</a>
<a
href="http://localhost:8103"
target="_blank"
class="text-blue-400 hover:text-blue-300 underline">Container logs</a
>
</li>
<li>
<a href="http://localhost:8104" target="_blank">Container stats</a>
<a
href="http://localhost:8104"
target="_blank"
class="text-blue-400 hover:text-blue-300 underline">Container stats</a
>
</li>
</ul>
<h2 class="text-lg font-bold m-4">Toast</h2>
<ul class="m-4">
<li>
<button on:click={() => triggerToast('Success')}>Trigger toast - Success</button>
<button
on:click={() => triggerToast('Success')}
class="text-green-400 hover:text-green-300 underline">Trigger toast - Success</button
>
</li>
<li>
<button on:click={() => triggerToast('Info')}>Trigger toast - Info</button>
<button
on:click={() => triggerToast('Info')}
class="text-blue-400 hover:text-blue-300 underline">Trigger toast - Info</button
>
</li>
<li>
<button on:click={() => triggerToast('Warning')}>Trigger toast - Warning</button>
<button
on:click={() => triggerToast('Warning')}
class="text-yellow-400 hover:text-yellow-300 underline"
>Trigger toast - Warning</button
>
</li>
<li>
<button on:click={() => triggerToast('Error')}>Trigger toast - Error</button>
<button
on:click={() => triggerToast('Error')}
class="text-red-400 hover:text-red-300 underline">Trigger toast - Error</button
>
</li>
</ul>
<div class="pt-4">
<h2 class="text-lg font-bold">Global State</h2>
<table class="border-2">
<table class="border-2 border-gray-400 dark:border-gray-500">
{#each Object.entries(state) as [key, value]}
<tr class="flex flex-col border-2 border-white">
<td class="p-4 font-bold border-1 border-white">{key}</td>
<td class="p-4 border-1 border-white w-full">
<tr class="flex flex-col border-2 border-white dark:border-gray-600">
<td class="p-4 font-bold border-1 border-white dark:border-gray-600">{key}</td>
<td class="p-4 border-1 border-white dark:border-gray-600 w-full">
{#if typeof value === 'object'}
<pre class="whitespace-pre-wrap">{JSON.stringify(value, null, 2)}</pre>
{:else}
@@ -70,7 +70,8 @@
.attr('y1', (height - margin.top - margin.bottom) / 2)
.attr('y2', (height - margin.top - margin.bottom) / 2)
.attr('stroke', '#E2E8F0')
.attr('stroke-width', 2);
.attr('stroke-width', 2)
.attr('class', 'timeline-line');
// X Axis
g.append('g')
@@ -340,7 +341,8 @@
.attr('y1', (height - margin.top - margin.bottom) / 2)
.attr('y2', (height - margin.top - margin.bottom) / 2)
.attr('stroke', '#E2E8F0')
.attr('stroke-width', 2);
.attr('stroke-width', 2)
.attr('class', 'timeline-line');
const ghostDots = 8;
const ghostDotsData = Array(ghostDots).fill(null);
@@ -355,6 +357,7 @@
.attr('r', 6)
.attr('fill', '#E2E8F0')
.attr('stroke', '#fff')
.attr('class', 'ghost-dot-fill')
.attr('stroke-width', 2);
}
@@ -378,10 +381,10 @@
try {
tooltip.innerHTML = `
<div class="p-2">
<div class="font-semibold text-lg text-gray-700 border-b-2">${toEvent(d.eventName).name}</div>
<div class="">${d.recipient?.email ?? ''}</div>
<div class="text-xs text-gray-600">${new Date(d.createdAt).toLocaleString()}</div>
${d.data ? `<div class="text-xs mt-1">${d.data}</div>` : ''}
<div class="font-semibold text-lg text-gray-700 dark:text-gray-200 border-b-2">${toEvent(d.eventName).name}</div>
<div class="text-gray-600 dark:text-gray-300">${d.recipient?.email ?? ''}</div>
<div class="text-xs text-gray-600 dark:text-gray-400">${new Date(d.createdAt).toLocaleString()}</div>
${d.data ? `<div class="text-xs mt-1 text-gray-500 dark:text-gray-400">${d.data}</div>` : ''}
</div>
`;
} catch (e) {
@@ -435,7 +438,10 @@
function getEventColor(eventName) {
if (!eventName) return '#6B7280'; // Default gray for undefined events
const colorMap = {
const isDark =
typeof document !== 'undefined' && document.documentElement.classList.contains('dark');
const lightModeColors = {
campaign_scheduled: '#4e68d8',
campaign_active: '#53afe3',
campaign_self_managed: '#303f9f',
@@ -443,7 +449,7 @@
campaign_recipient_scheduled: '#4e68d8',
campaign_recipient_message_sent: '#94cae6',
campaign_recipient_message_failed: '#f2bb58',
campaign_recipient_message_read: '#4cb5b5',
campaign_recipient_message_read: '#22c55e', // More muted green
campaign_recipient_before_page_visited: '#eea5fa',
campaign_recipient_page_visited: '#f96dcf',
campaign_recipient_after_page_visited: '#f6287b',
@@ -451,6 +457,25 @@
campaign_recipient_cancelled: '#161692',
default: '#6B7280'
};
const darkModeColors = {
campaign_scheduled: '#60a5fa',
campaign_active: '#38bdf8',
campaign_self_managed: '#6366f1',
campaign_closed: '#a1a1aa',
campaign_recipient_scheduled: '#60a5fa',
campaign_recipient_message_sent: '#7dd3fc',
campaign_recipient_message_failed: '#fbbf24',
campaign_recipient_message_read: '#4ade80', // Softer green for dark mode
campaign_recipient_before_page_visited: '#d8b4fe',
campaign_recipient_page_visited: '#f472b6',
campaign_recipient_after_page_visited: '#fb7185',
campaign_recipient_submitted_data: '#f87171',
campaign_recipient_cancelled: '#3730a3',
default: '#9ca3af'
};
const colorMap = isDark ? darkModeColors : lightModeColors;
return colorMap[eventName] || colorMap.default;
}
@@ -465,11 +490,11 @@
bind:this={svg}
width="100%"
height={height + 20}
class="bg-white rounded-lg shadow-sm p-2"
class="bg-white dark:bg-gray-800 rounded-lg shadow-sm p-2 transition-colors duration-200"
/>
<div
bind:this={tooltip}
class="absolute hidden bg-white shadow-lg rounded-lg border border-gray-200 z-10 pointer-events-none"
class="absolute hidden bg-white dark:bg-gray-800 shadow-lg rounded-lg border border-gray-200 dark:border-gray-600 z-10 pointer-events-none transition-colors duration-200"
/>
{#if !isGhost}
<div class="absolute top-2 right-2 flex gap-2">
@@ -478,23 +503,23 @@
use24Hour = !use24Hour;
updateTimeline();
}}
class="px-2 py-1 text-xs bg-white border border-slate-200 rounded shadow-sm hover:bg-slate-50 text-slate-600"
class="px-2 py-1 text-xs bg-white dark:bg-gray-700 border border-slate-200 dark:border-gray-600 rounded shadow-sm hover:bg-slate-50 dark:hover:bg-gray-600 text-slate-600 dark:text-gray-300 transition-colors duration-200"
>
{use24Hour ? '12h' : '24h'}
</button>
<button
on:click={resetZoom}
class="px-2 py-1 text-xs bg-white border border-slate-200 rounded shadow-sm hover:bg-slate-50 text-slate-600"
class="px-2 py-1 text-xs bg-white dark:bg-gray-700 border border-slate-200 dark:border-gray-600 rounded shadow-sm hover:bg-slate-50 dark:hover:bg-gray-600 text-slate-600 dark:text-gray-300 transition-colors duration-200"
>
Reset View
</button>
</div>
<div
class="absolute top-0 left-1/2 transform -translate-x-1/2 bg-white px-3 py-1 rounded-b-lg shadow-sm border border-t-0 text-sm font-medium text-slate-700"
class="absolute top-0 left-1/2 transform -translate-x-1/2 bg-white dark:bg-gray-800 px-3 py-1 rounded-b-lg shadow-sm border border-t-0 border-gray-200 dark:border-gray-600 text-sm font-medium text-slate-700 dark:text-gray-200 transition-colors duration-200"
>
{currentCenterDate}
</div>
<div class="absolute bottom-0 right-0 p-2 text-xs text-slate-500">
<div class="absolute bottom-0 right-0 p-2 text-xs text-slate-500 dark:text-gray-400">
Drag to pan • Scroll to zoom
</div>
{/if}
@@ -507,12 +532,45 @@
font-weight: 500;
fill: #4a5568;
}
:global(.dark .x-axis text) {
fill: #d1d5db;
}
:global(.x-axis line) {
stroke: #e2e8f0;
}
:global(.dark .x-axis line) {
stroke: #4b5563;
}
:global(.x-axis path) {
stroke: #e2e8f0;
}
:global(.dark .x-axis path) {
stroke: #4b5563;
}
:global(.timeline-line) {
stroke: #e2e8f0;
}
:global(.dark .timeline-line) {
stroke: #4b5563;
}
:global(.ghost-dot-fill) {
fill: #e2e8f0;
stroke: #fff;
}
:global(.dark .ghost-dot-fill) {
fill: #4b5563;
stroke: #1f2937;
}
:global(.ghost-dot) {
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
}
+4 -4
View File
@@ -43,7 +43,7 @@
<label class="flex flex-col py-2 w-56">
<div class="flex items-center">
<p class="font-semibold text-slate-600 py-2">
<p class="font-semibold text-slate-600 dark:text-gray-300 py-2 transition-colors duration-200">
<slot />
</p>
{#if toolTipText.length > 0}
@@ -52,8 +52,8 @@
</ToolTip>
{/if}
{#if optional === true}
<div class="bg-gray-100 ml-2 px-2 rounded-md">
<p class="text-slate-600 text-xs">optional</p>
<div class="bg-gray-100 dark:bg-gray-700 ml-2 px-2 rounded-md transition-colors duration-200">
<p class="text-slate-600 dark:text-gray-300 text-xs">optional</p>
</div>
{/if}
</div>
@@ -68,6 +68,6 @@
{multiple}
{required}
{placeholder}
class="border-solid border-2 py-2 px-2 rounded-md file:px-4 file:py-2 file:text-white file:cursor-pointer file:text-sm file:font-semibold bg-white file:bg-cta-green hover:cursor-pointer file:hover:bg-teal-300 file:border-hidden file:rounded-md"
class="border-solid border-2 border-gray-300 dark:border-gray-600 py-2 px-2 rounded-md file:px-4 file:py-2 file:text-white file:cursor-pointer file:text-sm file:font-semibold bg-white dark:bg-gray-700 file:bg-cta-blue hover:cursor-pointer file:hover:bg-blue-600 dark:file:bg-indigo-600 dark:file:hover:bg-indigo-700 file:border-hidden file:rounded-md text-gray-900 dark:text-white transition-colors duration-200"
/>
</label>
+1 -1
View File
@@ -6,7 +6,7 @@
<form
on:submit|preventDefault
class="flex w-60 flex-col"
class="flex w-60 flex-col bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 transition-colors duration-200"
class:h-full={fullHeight}
class:w-full={fullWidth}
bind:this={bindTo}
@@ -9,7 +9,7 @@
class:w-32={size === 'small'}
class:w-36={size === 'medium'}
class:w-60={size === 'large'}
class="bg-cta-blue px-2 py-2 self-end hover:bg-blue-500 text-white text-sm font-bold flex justify-center items-center uppercase rounded-md"
class="bg-cta-blue dark:bg-blue-600 px-2 py-2 self-end hover:bg-blue-500 dark:hover:bg-blue-700 text-white text-sm font-bold flex justify-center items-center uppercase rounded-md transition-colors duration-200 disabled:opacity-50 dark:disabled:opacity-60"
on:click
>
{#if isSubmitting}
@@ -3,7 +3,7 @@
</script>
<div
class="px-6 flex flex-col items-start sm:items-start md:items-start lg:items-start xl:items-start 2xl:items-start"
class="px-6 flex flex-col items-start sm:items-start md:items-start lg:items-start xl:items-start 2xl:items-start text-gray-900 dark:text-gray-100 transition-colors duration-200"
class:overflow-x-scroll={overflowX}
>
<slot />
@@ -3,7 +3,7 @@
</script>
<div
class="scrollX col-start-1 col-span-3 row-start-1 row-span-5 py-8 flex-col flex sm:flex-col md:flex-col lg:flex-row justify-center"
class="scrollX col-start-1 col-span-3 row-start-1 row-span-5 py-8 flex-col flex sm:flex-col md:flex-col lg:flex-row justify-center bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 transition-colors duration-200"
{id}
>
<slot />
+2 -2
View File
@@ -7,7 +7,7 @@
<div class="flex col-span-12 justify-center p-4">
<div class="w-80 flex col-span-12 justify-center">
<div
class="flex items-center w-full bg-pleasant-gray rounded-md border text-center p-2 font-titilium"
class="flex items-center w-full bg-pleasant-gray dark:bg-gray-700 rounded-md border border-gray-200 dark:border-gray-600 text-center p-2 font-titilium transition-colors duration-200"
>
<svg class="w-12 ml-4" transition:slide viewBox="0 0 32.94 32.94">
<circle
@@ -33,7 +33,7 @@
/>
</g>
</svg>
<p class="pl-4">
<p class="pl-4 text-gray-700 dark:text-gray-200 transition-colors duration-200">
{message}
</p>
</div>
+1 -1
View File
@@ -4,7 +4,7 @@
<form
on:submit|preventDefault
class="col-start-1 col-span-3 row-start-1 row-span-5 py-8 flex-col flex sm:flex-col md:flex-col lg:flex-row justify-start"
class="col-start-1 col-span-3 row-start-1 row-span-5 py-8 flex-col flex sm:flex-col md:flex-col lg:flex-row justify-start bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 transition-colors duration-200"
bind:this={bindTo}
>
<slot />
@@ -19,12 +19,12 @@
</script>
<div
class="py-4 row-span-2 col-start-1 col-span-3 border-t-2 w-full flex flex-row justify-center items-center sm:justify-center md:justify-center lg:justify-end xl:justify-end 2xl:justify-end"
class="py-4 row-span-2 col-start-1 col-span-3 border-t-2 border-gray-200 dark:border-gray-700 w-full flex flex-row justify-center items-center sm:justify-center md:justify-center lg:justify-end xl:justify-end 2xl:justify-end bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 transition-colors duration-200"
>
<button
type="reset"
on:click|preventDefault={onCloseModal}
class="bg-slate-400 hover:bg-slate-300 text-sm mr-2 uppercase font-bold px-4 py-2 text-white rounded-md"
class="bg-slate-400 hover:bg-slate-300 dark:bg-slate-600 dark:hover:bg-slate-500 text-sm mr-2 uppercase font-bold px-4 py-2 text-white rounded-md transition-colors duration-200"
>{closeText}</button
>
<FormButton {isSubmitting}>{okText}</FormButton>
+1 -1
View File
@@ -7,7 +7,7 @@
<form
on:submit|preventDefault
inert={isSubmitting}
class="grid grid-cols-3 grid-rows-1 w-full h-full flex-col"
class="grid grid-cols-3 grid-rows-1 w-full h-full flex-col bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 transition-colors duration-200"
class:opacity-70={isSubmitting}
bind:this={bindTo}
{novalidate}
+4 -4
View File
@@ -4,18 +4,18 @@
const widths = ['w-16', 'w-20', 'w-24'];
let height = 'h-4';
let width = widths[Math.floor(Math.random() * widths.length)];
let gradient = 'from-gray-300 to-transparent bg-gradient-to-r';
let gradient = 'from-gray-300 dark:from-gray-600 to-transparent bg-gradient-to-r';
if (square) {
width = 'h-4';
height = 'w-4';
gradient = 'bg-gray-300';
gradient = 'bg-gray-300 dark:bg-gray-600';
}
</script>
{#if center}
<div class="flex items-center justify-center">
<div class="{width} {height} {gradient} ">&nbsp;</div>
<div class="{width} {height} {gradient} transition-colors duration-200">&nbsp;</div>
</div>
{:else}
<div class="{width} {height} {gradient}">&nbsp;</div>
<div class="{width} {height} {gradient} transition-colors duration-200">&nbsp;</div>
{/if}
@@ -1,3 +1,5 @@
<div class="bg-gray-300 w-full h-1/10 flex justify-center">
<img alt="logo" src="/logo-white.svg" class="w-1/3 sm:w-1/4 lg:w-2/10 xl:w-1/10 2xl:w-1/10" />
</div>
<div
class="bg-gray-300 dark:bg-gray-700 w-full h-1/10 flex justify-center transition-colors duration-200"
>
<img alt="logo" src="/logo-white.svg" class="w-1/3 sm:w-1/4 lg:w-2/10 xl:w-1/10 2xl:w-1/10" />
</div>
+3 -1
View File
@@ -1,3 +1,5 @@
<h1 class="text-3xl text-gray-600 font-bold uppercase">
<h1
class="text-3xl text-gray-600 dark:text-gray-300 font-bold uppercase transition-colors duration-200"
>
<slot />
</h1>
+2 -2
View File
@@ -1,8 +1,8 @@
<script>
export let name = 'World';
console.log(name)
console.log(name);
</script>
<main>
<main class="text-gray-900 dark:text-gray-100 transition-colors duration-200">
Hello {name}!
</main>
+4 -2
View File
@@ -10,7 +10,9 @@
</script>
<div class="flex flex-col w-full p-4 h-24">
<label for={fieldName} class="text-md font-semibold font-titilium text-pc-darkblue"
<label
for={fieldName}
class="text-md font-semibold font-titilium text-pc-darkblue dark:text-gray-200"
>{fieldName}</label
>
<input
@@ -32,6 +34,6 @@
{type}
id={fieldName}
name={fieldName}
class="w-full p-2 rounded bg-pc-lightblue focus:outline-none focus:ring-0 focus:border-cta-blue focus:border-2"
class="w-full p-2 rounded bg-pc-lightblue dark:bg-gray-700 dark:border-gray-600 dark:text-white dark:placeholder-gray-400 focus:outline-none focus:ring-0 focus:border-cta-blue dark:focus:border-blue-500 focus:border-2 transition-colors duration-200"
/>
</div>
+5 -2
View File
@@ -15,13 +15,16 @@
</script>
{#if $isLoading && isAnimating}
<div class="fixed top-0 left-0 w-full h-full opacity-[0.5]" transition:blur={{ duration }} />
<div
class="fixed top-0 left-0 w-full h-full bg-black dark:bg-gray-900 opacity-[0.5] transition-colors duration-200"
transition:blur={{ duration }}
/>
<div
transition:blur={{ duration }}
class="fixed top-0 left-0 w-full h-full flex justify-center items-center backdrop-blur-sm z-50"
>
<div
class="w-20 h-20 border-t-8 border-t-cta-blue border-r-8 border-r-cta-blue border-b-cta-blue border-b-8 border-l-transparent border-l-8 rounded-full animate-spin"
class="w-20 h-20 border-t-8 border-t-cta-blue dark:border-t-blue-500 border-r-8 border-r-cta-blue dark:border-r-blue-500 border-b-cta-blue dark:border-b-blue-500 border-b-8 border-l-transparent border-l-8 rounded-full animate-spin transition-colors duration-200"
></div>
</div>
{/if}
+5 -3
View File
@@ -240,7 +240,9 @@
{#if visible}
<div bind:this={bindTo}>
<div class="fixed top-0 left-0 w-full h-full bg-cta-blue opacity-20 blur-xl" />
<div
class="fixed top-0 left-0 w-full h-full bg-cta-blue dark:bg-gray-900 opacity-20 blur-xl transition-colors duration-200"
/>
<div
class="fixed top-0 left-0 w-full h-full flex justify-center items-center backdrop-blur-sm z-20"
role="dialog"
@@ -250,11 +252,11 @@
>
<section
bind:this={modalElement}
class="shadow-xl w-auto ml-20 mr-8 max-h-[90vh] bg-white opacity-100 rounded-md flex flex-col"
class="shadow-xl dark:shadow-gray-900/70 w-auto ml-20 mr-8 max-h-[90vh] bg-white dark:bg-gray-800 opacity-100 rounded-md flex flex-col transition-colors duration-200"
>
<div
class:opacity-20={isSubmitting}
class="bg-cta-blue text-white rounded-t-md py-4 px-8 flex justify-between flex-shrink-0"
class="bg-cta-blue dark:bg-blue-700 text-white rounded-t-md py-4 px-8 flex justify-between flex-shrink-0 transition-colors duration-200"
>
<div class="flex-1">
<h1 id="modal-title" class="uppercase mr-8 font-semibold text-2xl">{headerText}</h1>
@@ -43,12 +43,16 @@
<div class="flex items-center mb-8 mt-4">
<button
class="bg-highlight-blue w-8 text-white hover:bg-active-blue m-1 rounded-md py-1 px-1"
class="bg-highlight-blue dark:bg-blue-600 w-8 text-white hover:bg-active-blue dark:hover:bg-blue-700 m-1 rounded-md py-1 px-1 transition-colors duration-200"
on:click|preventDefault={previousPage}>&lt;&lt;</button
>
<div class="w-8 text-center bg-grayblue-light rounded-md py-1 px-1">{currentPage}</div>
<div
class="w-8 text-center bg-grayblue-light dark:bg-gray-700 text-gray-700 dark:text-gray-200 rounded-md py-1 px-1 transition-colors duration-200"
>
{currentPage}
</div>
<button
class="bg-highlight-blue w-8 text-white hover:bg-active-blue m-1 rounded-md py-1 px-1"
class="bg-highlight-blue dark:bg-blue-600 w-8 text-white hover:bg-active-blue dark:hover:bg-blue-700 m-1 rounded-md py-1 px-1 transition-colors duration-200"
on:click|preventDefault={nextPage}>&gt;&gt;</button
>
</div>
@@ -59,7 +59,7 @@
<label class="flex flex-col py-2 w-60">
<div class="flex items-center">
<p class="font-semibold text-slate-600 py-2">
<p class="font-semibold text-slate-600 dark:text-gray-300 py-2 transition-colors duration-200">
<slot />
</p>
{#if toolTipText.length > 0}
@@ -68,8 +68,10 @@
</ToolTip>
{/if}
{#if optional === true}
<div class="bg-gray-100 ml-2 px-2 rounded-md">
<p class="text-slate-600 text-xs">optional</p>
<div class="bg-gray-100 dark:bg-gray-700 ml-2 px-2 rounded-md transition-colors duration-200">
<p class="text-slate-600 dark:text-gray-300 text-xs transition-colors duration-200">
optional
</p>
</div>
{/if}
</div>
@@ -86,7 +88,7 @@
minlength={minLength}
maxlength={maxLength}
{required}
class="text-ellipsis w-60 rounded-md py-2 pl-4 text-gray-600 border border-transparent focus:outline-none focus:border-solid focus:border-slate-400 focus:bg-gray-100 bg-grayblue-light font-normal"
class="text-ellipsis w-60 rounded-md py-2 pl-4 pr-12 text-gray-600 dark:text-gray-200 border border-transparent dark:border-gray-600 focus:outline-none focus:border-solid focus:border-slate-400 dark:focus:border-blue-500 focus:bg-gray-100 dark:focus:bg-gray-600 bg-grayblue-light dark:bg-gray-700 font-normal transition-colors duration-200"
/>
{:else}
<input
@@ -100,18 +102,26 @@
{placeholder}
autocomplete="off"
{required}
class="text-ellipsis w-60 rounded-md py-2 pl-4 text-gray-600 border border-transparent focus:outline-none focus:border-solid focus:border-slate-400 focus:bg-gray-100 bg-grayblue-light font-normal"
class="text-ellipsis w-60 rounded-md py-2 pl-4 pr-12 text-gray-600 dark:text-gray-200 border border-transparent dark:border-gray-600 focus:outline-none focus:border-solid focus:border-slate-400 dark:focus:border-blue-500 focus:bg-gray-100 dark:focus:bg-gray-600 bg-grayblue-light dark:bg-gray-700 font-normal transition-colors duration-200"
/>
{/if}
<button
class="absolute w-8 mr-2 hover:opacity-70"
class="absolute w-8 mr-2 hover:opacity-70 transition-opacity duration-200"
on:click={handleClick}
on:keyup={handleClick}
>
{#if viewPassword}
<img src="/view.svg" alt="view" />
<img
src="/view.svg"
alt="view"
class="dark:filter dark:brightness-0 dark:invert transition-all duration-200"
/>
{:else}
<img src="/toggle-view.svg" alt="toggle view" />
<img
src="/toggle-view.svg"
alt="toggle view"
class="dark:filter dark:brightness-0 dark:invert transition-all duration-200"
/>
{/if}
</button>
</div>
@@ -88,6 +88,7 @@
: value instanceof Date
? value.toLocaleString()
: ''}
class="text-gray-600 dark:text-gray-400 transition-colors duration-200"
>
{formattedTime}
</span>
@@ -30,4 +30,6 @@
});
</script>
<div class="w-full h-full flex border-4 border-pc-darkblue animate-pulse"></div>
<div
class="w-full h-full flex border-4 border-pc-darkblue dark:border-gray-600 animate-pulse transition-colors duration-200"
></div>
+1 -1
View File
@@ -39,7 +39,7 @@
setSearch();
}
}}
class="bg-grayblue-light w-56 border text-gray-600 border-gray-300 pl-8 py-2 relative rounded-lg focus:outline-none focus:ring-0 focus:border-cta-blue focus:border"
class="bg-grayblue-light dark:bg-gray-700 w-56 border text-gray-600 dark:text-gray-200 border-gray-300 dark:border-gray-600 pl-8 py-2 relative rounded-lg focus:outline-none focus:ring-0 focus:border-cta-blue dark:focus:border-blue-500 focus:border transition-colors duration-200 dark:placeholder-gray-400"
placeholder="Search"
/>
</div>
+5 -3
View File
@@ -21,10 +21,12 @@
});
</script>
<div >
<label for="pet-select">Show:</label>
<div>
<label for="pet-select" class="text-gray-600 dark:text-gray-300 transition-colors duration-200"
>Show:</label
>
<select
class="bg-grayblue-light px-2 py-1 rounded-md text-gray-600 border border-transparent focus:outline-none focus:border-solid focus:border focus:border-slate-400 focus:bg-gray-100"
class="bg-grayblue-light dark:bg-gray-700 px-2 py-1 rounded-md text-gray-600 dark:text-gray-200 border border-transparent dark:border-gray-600 focus:outline-none focus:border-solid focus:border focus:border-slate-400 dark:focus:border-blue-500 focus:bg-gray-100 dark:focus:bg-gray-600 transition-colors duration-200"
name="entries"
id="entries"
bind:value
@@ -15,15 +15,19 @@
<div class="flex flex-col gap-2 py-2">
{#if label}
<div class="flex flex-row">
<div class="font-semibold text-slate-600">{label}</div>
<div class="font-semibold text-slate-600 dark:text-gray-300 transition-colors duration-200">
{label}
</div>
{#if toolTipText.length > 0}
<ToolTip>
{toolTipText}
</ToolTip>
{/if}
{#if optional}
<div class="bg-gray-100 ml-2 px-2 rounded-md">
<p class="text-slate-600 text-xs">optional</p>
<div
class="bg-gray-100 dark:bg-gray-700 ml-2 px-2 rounded-md transition-colors duration-200"
>
<p class="text-slate-600 dark:text-gray-300 text-xs">optional</p>
</div>
{/if}
</div>
@@ -42,11 +46,11 @@
p-3 rounded-lg border-2 transition-all duration-200
flex flex-col items-center justify-center text-center
w-40
hover:border-blue-300 hover:bg-blue-50
hover:border-blue-300 dark:hover:border-blue-500 hover:bg-blue-50 dark:hover:bg-blue-900/30
${
value === option.value
? 'border-green-500 bg-green-50 text-green-700 '
: 'border-gray-200 bg-white text-gray-700 '
? 'border-green-500 dark:border-green-400 bg-green-50 dark:bg-green-900/30 text-green-700 dark:text-green-300'
: 'border-gray-200 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-200'
}
`}
on:click={() => {
@@ -59,7 +63,9 @@
{/if}
<span class="font-medium text-sm">{option.label}</span>
{#if option.description}
<span class="text-xs text-gray-500 mt-1">{option.description}</span>
<span class="text-xs text-gray-500 dark:text-gray-400 mt-1 transition-colors duration-200"
>{option.description}</span
>
{/if}
</button>
{/each}
+16 -6
View File
@@ -43,12 +43,20 @@
</script>
<div
class="bg-white p-6 rounded-lg shadow-md border-l-[12px] {borderColor} hover:shadow-lg transition-shadow"
class="bg-white dark:bg-gray-800 p-6 rounded-lg shadow-md dark:shadow-gray-900/50 border-l-[12px] {borderColor} hover:shadow-lg dark:hover:shadow-gray-900/70 transition-all duration-200"
>
<div class="text-grayblue-dark text-sm font-semibold">{title}</div>
<div
class="text-grayblue-dark dark:text-gray-400 text-sm font-semibold transition-colors duration-200"
>
{title}
</div>
<div class="flex items-center justify-between">
<div class="flex items-center">
<span class="text-3xl font-bold text-pc-darkblue {flash ? 'flash' : ''}">
<span
class="text-3xl font-bold text-pc-darkblue dark:text-gray-100 transition-colors duration-200 {flash
? 'flash'
: ''}"
>
{Math.floor($displayValue)}
</span>
<div class="ml-2">
@@ -74,12 +82,14 @@
{#if validPercentages.length > 0}
<button
class="mt-2 text-sm text-gray-600 flex items-center"
class="mt-2 text-sm text-gray-600 dark:text-gray-400 flex items-center transition-colors duration-200"
on:click={cyclePercentage}
class:cursor-pointer={validPercentages.length > 1}
>
<div class="flex items-center">
<span class="text-pc-darkblue font-semibold">
<span
class="text-pc-darkblue dark:text-gray-200 font-semibold transition-colors duration-200"
>
{validPercentages[currentPercentageIndex].value}%
</span>
<span class="ml-1">
@@ -89,7 +99,7 @@
{#if validPercentages.length > 1}
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-4 w-4 ml-1 text-gray-400"
class="h-4 w-4 ml-1 text-gray-400 dark:text-gray-500 transition-colors duration-200"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
@@ -1,3 +1,5 @@
<h2 class="text-lg text-gray-600 font-bold uppercase">
<h2
class="text-lg text-gray-600 dark:text-gray-300 font-bold uppercase transition-colors duration-200"
>
<slot />
</h2>
+4 -1
View File
@@ -1,3 +1,6 @@
<span title="Test / Preview" class="select-none px-1 border-2 relative -top-1 rounded-lg text-xs">
<span
title="Test / Preview"
class="select-none px-1 border-2 border-gray-400 dark:border-gray-500 relative -top-1 rounded-lg text-xs text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-800 transition-colors duration-200"
>
test
</span>
+6 -4
View File
@@ -59,7 +59,7 @@
<label class="flex flex-col py-2">
<div class="flex items-center">
<p class="font-semibold text-slate-600 py-2">
<p class="font-semibold text-slate-600 dark:text-gray-300 py-2 transition-colors duration-200">
<slot />
</p>
{#if toolTipText.length > 0}
@@ -68,8 +68,10 @@
</ToolTip>
{/if}
{#if optional === true}
<div class="bg-gray-100 ml-2 px-2 rounded-md">
<p class="text-slate-600 text-xs">optional</p>
<div class="bg-gray-100 dark:bg-gray-700 ml-2 px-2 rounded-md transition-colors duration-200">
<p class="text-slate-600 dark:text-gray-300 text-xs transition-colors duration-200">
optional
</p>
</div>
{/if}
</div>
@@ -93,7 +95,7 @@
{required}
{placeholder}
{pattern}
class="text-ellipsis row-start-1 row-span-3 justify-self-center rounded-md py-2 pl-2 text-gray-600 border border-transparent focus:outline-none focus:border-solid focus:border-slate-400 focus:bg-gray-100 bg-grayblue-light font-normal"
class="text-ellipsis row-start-1 row-span-3 justify-self-center rounded-md py-2 pl-2 text-gray-600 dark:text-gray-200 border border-transparent dark:border-gray-600 focus:outline-none focus:border-solid focus:border-slate-400 dark:focus:border-blue-500 focus:bg-gray-100 dark:focus:bg-gray-600 bg-grayblue-light dark:bg-gray-700 font-normal transition-colors duration-200"
class:w-24={width === 'small'}
class:w-60={width === 'medium'}
class:w-95={width === 'large'}
@@ -117,7 +117,9 @@
<div class="flex flex-col justify-start">
<label class="flex flex-col py-2 relative">
<div class="flex items-center">
<p class="font-semibold text-slate-600 py-2">
<p
class="font-semibold text-slate-600 dark:text-gray-300 py-2 transition-colors duration-200"
>
<slot />
</p>
{#if toolTipText.length > 0}
@@ -126,8 +128,10 @@
</ToolTip>
{/if}
{#if optional === true}
<div class="bg-gray-100 ml-2 px-2 rounded-md">
<p class="text-slate-600 text-xs">optional</p>
<div
class="bg-gray-100 dark:bg-gray-700 ml-2 px-2 rounded-md transition-colors duration-200"
>
<p class="text-slate-600 dark:text-gray-300 text-xs">optional</p>
</div>
{/if}
</div>
@@ -151,7 +155,7 @@
{id}
required={required && !value.length}
autocomplete="off"
class="w-full relative rounded-md py-2 pl-4 focus:pl-10 text-gray-600 border border-transparent focus:outline-none focus:border-solid focus:border focus:border-slate-400 focus:bg-gray-100 bg-grayblue-light font-normal cursor-pointer focus:cursor-text"
class="w-full relative rounded-md py-2 pl-4 focus:pl-10 text-gray-600 dark:text-gray-300 border border-transparent focus:outline-none focus:border-solid focus:border focus:border-slate-400 dark:focus:border-gray-500 focus:bg-gray-100 dark:focus:bg-gray-600 bg-grayblue-light dark:bg-gray-700 font-normal cursor-pointer focus:cursor-text transition-colors duration-200"
/>
{#if showSelection}
<img
@@ -165,13 +169,13 @@
{#if showSelection}
<div class="w-60 absolute top-10 z-50">
<ul
class="bg-gray-100 list-none mt-4 rounded-md min-w-fit shadow-md border max-h-40 overflow-y-scroll"
class="bg-gray-100 dark:bg-gray-700 list-none mt-4 rounded-md min-w-fit shadow-md border border-gray-200 dark:border-gray-600 max-h-40 overflow-y-scroll transition-colors duration-200"
>
{#if options.length}
{#each filteredOptions as option}
<li>
<button
class="w-full text-left bg-slate-100 rounded-md text-gray-600 hover:bg-grayblue-dark hover:text-white py-2 px-2 cursor-pointer"
class="w-full text-left bg-slate-100 dark:bg-gray-600 rounded-md text-gray-600 dark:text-gray-200 hover:bg-grayblue-dark dark:hover:bg-gray-500 hover:text-white py-2 px-2 cursor-pointer transition-colors duration-200"
on:click={() => {
onClickSelectedOption(option);
}}
@@ -181,7 +185,11 @@
</li>
{/each}
{:else}
<li class="w-full bg-slate-100 rounded-md text-gray-600 py-2 px-2">List is empty</li>
<li
class="w-full bg-slate-100 dark:bg-gray-600 rounded-md text-gray-600 dark:text-gray-300 py-2 px-2 transition-colors duration-200"
>
List is empty
</li>
{/if}
</ul>
</div>
@@ -192,7 +200,7 @@
on:click|preventDefault={removeSelection}
on:keypress|preventDefault={removeSelection}
data-value={option}
class="flex flex-row items-center bg-gray-100 hover:bg-gray-200 px-2 py-2 mt-2 mr-2 rounded-md"
class="flex flex-row items-center bg-gray-100 dark:bg-gray-700 hover:bg-gray-200 dark:hover:bg-gray-600 px-2 py-2 mt-2 mr-2 rounded-md text-gray-900 dark:text-gray-100 transition-colors duration-200"
>
{option}
<img class="w-4 ml-2 pointer-events-none" src="/delete2.svg" alt="delete" />
@@ -71,7 +71,9 @@
<div class="flex flex-col justify-start">
<label class="flex flex-col py-2 relative">
<div class="flex items-center">
<p class="font-semibold text-slate-600 py-2">
<p
class="font-semibold text-slate-600 dark:text-gray-300 py-2 transition-colors duration-200"
>
<slot />
</p>
{#if toolTipText.length > 0}
@@ -97,7 +99,7 @@
on:keyup={_onKeyUp}
on:click|stopPropagation={() => {}}
autocomplete="off"
class="w-full relative rounded-md py-2 pl-4 focus:pl-10 text-gray-600 border border-transparent focus:outline-none focus:border-solid focus:border focus:border-slate-400 focus:bg-gray-100 bg-grayblue-light font-normal cursor-pointer focus:cursor-text"
class="w-full relative rounded-md py-2 pl-4 focus:pl-10 text-gray-600 dark:text-gray-100 border border-transparent focus:outline-none focus:border-solid focus:border focus:border-slate-400 dark:focus:border-gray-500 focus:bg-gray-100 dark:focus:bg-gray-700 bg-grayblue-light dark:bg-gray-600 font-normal cursor-pointer focus:cursor-text transition-colors duration-200"
{id}
{required}
/>
@@ -109,12 +111,12 @@
{#if options.length && showSelection}
<div class="w-96 absolute top-10 z-50">
<ul
class="bg-gray-100 list-none mt-4 rounded-md min-w-fit shadow-md border max-h-40 overflow-y-scroll"
class="bg-gray-100 dark:bg-gray-700 list-none mt-4 rounded-md min-w-fit shadow-md dark:shadow-gray-900/50 border border-gray-200 dark:border-gray-600 max-h-40 overflow-y-scroll transition-colors duration-200"
>
{#each options as option}
<li class="break-words">
<button
class="w-full text-left bg-slate-100 rounded-md text-gray-600 hover:bg-grayblue-dark hover:text-white py-2 px-2 cursor-pointer"
class="w-full text-left bg-slate-100 dark:bg-gray-700 rounded-md text-gray-600 dark:text-gray-200 hover:bg-grayblue-dark dark:hover:bg-gray-600 hover:text-white py-2 px-2 cursor-pointer transition-colors duration-200"
on:click|preventDefault={() => {
value = '';
showSelection = false;
@@ -223,7 +223,10 @@
>
<label class="flex flex-col py-2 relative" class:py-2={!inline} class:pr-2={inline}>
<div class="flex items-center">
<p id={labelId} class="font-semibold text-slate-600 py-1">
<p
id={labelId}
class="font-semibold text-slate-600 dark:text-gray-300 py-1 transition-colors duration-200"
>
<slot />
</p>
{#if toolTipText.length > 0}
@@ -232,8 +235,10 @@
</ToolTip>
{/if}
{#if optional === true}
<div class="bg-gray-100 ml-2 px-2 rounded-md">
<p class="text-slate-600 text-xs">optional</p>
<div
class="bg-gray-100 dark:bg-gray-700 ml-2 px-2 rounded-md transition-colors duration-200"
>
<p class="text-slate-600 dark:text-gray-300 text-xs">optional</p>
</div>
{/if}
</div>
@@ -261,7 +266,7 @@
on:keydown={handleKeyDown}
on:click={handleFocus}
autocomplete="off"
class="w-full relative rounded-md py-2 pr-10 text-gray-600 border border-transparent focus:outline-none focus:border-solid focus:border focus:border-slate-400 focus:bg-gray-100 bg-grayblue-light font-normal cursor-pointer focus:cursor-text"
class="w-full relative rounded-md py-2 pr-10 text-gray-600 dark:text-gray-300 border border-transparent focus:outline-none focus:border-solid focus:border focus:border-slate-400 dark:focus:border-gray-500 focus:bg-gray-100 dark:focus:bg-gray-600 bg-grayblue-light dark:bg-gray-700 font-normal cursor-pointer focus:cursor-text transition-colors duration-200"
class:pl-10={showDropdown}
class:pl-4={!showDropdown}
class:text-gray-400={!hasValue && !showDropdown}
@@ -314,7 +319,7 @@
id={listboxId}
role="listbox"
aria-labelledby={labelId}
class="bg-gray-100 list-none mt-4 z-[999] rounded-md min-w-fit shadow-md border max-h-40 overflow-y-scroll"
class="bg-gray-100 dark:bg-gray-700 list-none mt-4 z-[999] rounded-md min-w-fit shadow-md border border-gray-200 dark:border-gray-600 max-h-40 overflow-y-scroll transition-colors duration-200"
>
{#if allOptions.length}
{#each allOptions as option, index}
@@ -323,7 +328,7 @@
id="{listboxId}-option-{index}"
role="option"
aria-selected={value === option}
class="w-full text-left bg-slate-100 rounded-md text-gray-600 hover:bg-grayblue-dark hover:text-white py-2 px-2 cursor-pointer focus:bg-grayblue-dark focus:text-white focus:outline-none"
class="w-full text-left bg-slate-100 dark:bg-gray-600 rounded-md text-gray-600 dark:text-gray-200 hover:bg-grayblue-dark dark:hover:bg-gray-500 hover:text-white py-2 px-2 cursor-pointer focus:bg-grayblue-dark dark:focus:bg-gray-500 focus:text-white focus:outline-none transition-colors duration-200"
on:click={(e) => {
e.preventDefault();
e.stopPropagation();
@@ -337,7 +342,10 @@
</li>
{/each}
{:else}
<li role="none" class="w-full bg-slate-100 rounded-md text-gray-600 py-2 px-2">
<li
role="none"
class="w-full bg-slate-100 dark:bg-gray-600 rounded-md text-gray-600 dark:text-gray-300 py-2 px-2 transition-colors duration-200"
>
No options available
</li>
{/if}
@@ -52,7 +52,7 @@
<label class="flex flex-col py-2 w-60" class:w-full={fullWidth}>
<div class="flex items-center">
<p class="font-bold text-slate-600 py-2">
<p class="font-bold text-slate-600 dark:text-gray-300 py-2 transition-colors duration-200">
<slot />
</p>
{#if toolTipText.length > 0}
@@ -61,8 +61,10 @@
</ToolTip>
{/if}
{#if optional === true}
<div class="bg-gray-100 ml-2 px-2 rounded-md">
<p class="text-slate-600 text-xs">optional</p>
<div class="bg-gray-100 dark:bg-gray-700 ml-2 px-2 rounded-md transition-colors duration-200">
<p class="text-slate-600 dark:text-gray-300 text-xs transition-colors duration-200">
optional
</p>
</div>
{/if}
</div>
@@ -76,7 +78,7 @@
maxlength={maxLength}
{readonly}
{placeholder}
class=" focus:outline-none pl-2 border border-transparent rounded-md focus:border-solid text-gray-600 focus:bg-gray-100 font-light focus:border-slate-400 bg-grayblue-light"
class=" focus:outline-none pl-2 border border-transparent dark:border-gray-600 rounded-md focus:border-solid text-gray-600 dark:text-gray-200 focus:bg-gray-100 dark:focus:bg-gray-600 font-light focus:border-slate-400 dark:focus:border-blue-500 bg-grayblue-light dark:bg-gray-700 transition-colors duration-200"
class:h-16={height === 'small'}
class:h-28={height === 'medium'}
class:h-48={height === 'large'}
@@ -0,0 +1,52 @@
<script>
import { theme, toggleMode, modeLight, modeDark } from '$lib/theme.js';
// reactive statement to determine current theme
$: isDark = $theme === modeDark;
const handleToggle = () => {
toggleMode();
};
</script>
<button
on:click={handleToggle}
class="relative inline-flex items-center justify-center w-6 h-6 rounded transition-colors duration-200 focus:outline-none"
title={isDark ? 'Switch to light mode' : 'Switch to dark mode'}
aria-label={isDark ? 'Switch to light mode' : 'Switch to dark mode'}
>
<!-- sun icon for light mode -->
<svg
class="w-4 h-4 text-yellow-400 transition-all duration-200 {isDark
? 'opacity-0 rotate-90 scale-0'
: 'opacity-100 rotate-0 scale-100'} absolute"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z"
clip-rule="evenodd"
/>
</svg>
<!-- moon icon for dark mode -->
<svg
class="w-4 h-4 text-blue-300 transition-all duration-200 {isDark
? 'opacity-100 rotate-0 scale-100'
: 'opacity-0 -rotate-90 scale-0'} absolute"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z" />
</svg>
<!-- invisible placeholder to maintain button size -->
<div class="w-4 h-4 opacity-0">
<svg class="w-4 h-4" viewBox="0 0 20 20">
<path d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1z" />
</svg>
</div>
</button>
+1 -1
View File
@@ -1,6 +1,6 @@
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-5 w-5 text-slate-400"
class="h-5 w-5 text-slate-400 dark:text-gray-400 transition-colors duration-200"
viewBox="0 0 20 20"
fill="currentColor"
>

Before

Width:  |  Height:  |  Size: 322 B

After

Width:  |  Height:  |  Size: 372 B

+4 -4
View File
@@ -30,7 +30,7 @@
}
}}
transition:slide|global
class="flex items-center bg-pleasant-gray text-gray-500 shadow-lg capitalize text-xl p-4 first:mt-0 mt-4 min-w-max rounded-md justify-self-center w-full"
class="flex items-center bg-pleasant-gray dark:bg-gray-700 text-gray-500 dark:text-gray-200 shadow-lg dark:shadow-gray-900/50 capitalize text-xl p-4 first:mt-0 mt-4 min-w-max rounded-md justify-self-center w-full transition-colors duration-200"
>
<!-- <img class="w-9 mr-6" draggable="false" src={src1} alt="checkmark success" /> -->
<svg class="w-11 mr-6" viewBox="0 0 32.25 32.4">
@@ -60,7 +60,7 @@
}
}}
transition:slide|global
class="flex items-center bg-pleasant-gray text-gray-500 shadow-lg capitalize text-xl p-4 first:mt-0 mt-4 min-w-max rounded-md justify-self-center w-full"
class="flex items-center bg-pleasant-gray dark:bg-gray-700 text-gray-500 dark:text-gray-200 shadow-lg dark:shadow-gray-900/50 capitalize text-xl p-4 first:mt-0 mt-4 min-w-max rounded-md justify-self-center w-full transition-colors duration-200"
>
<!-- <img class="w-9 mr-6" draggable="false" src="/t-warning.svg" alt="checkmark warning" /> -->
<svg class="w-11 mr-6" viewBox="0 0 36.26 32.41">
@@ -101,7 +101,7 @@
}
}}
transition:slide|global
class="flex items-center bg-pleasant-gray text-gray-500 shadow-lg capitalize text-xl p-4 first:mt-0 mt-4 min-w-max rounded-md justify-self-center w-full"
class="flex items-center bg-pleasant-gray dark:bg-gray-700 text-gray-500 dark:text-gray-200 shadow-lg dark:shadow-gray-900/50 capitalize text-xl p-4 first:mt-0 mt-4 min-w-max rounded-md justify-self-center w-full transition-colors duration-200"
>
<!-- <img class="w-9 mr-6" draggable="false" src="/t-error.svg" alt="checkmark error" /> -->
<svg class="w-11 mr-6" viewBox="0 0 32.98 32.98">
@@ -143,7 +143,7 @@
removeToast(toast.id);
}
}}
class="flex items-center bg-pleasant-gray text-gray-500 shadow-lg capitalize text-xl p-4 first:mt-0 mt-4 min-w-max rounded-md justify-self-center w-full"
class="flex items-center bg-pleasant-gray dark:bg-gray-700 text-gray-500 dark:text-gray-200 shadow-lg dark:shadow-gray-900/50 capitalize text-xl p-4 first:mt-0 mt-4 min-w-max rounded-md justify-self-center w-full transition-colors duration-200"
>
<!-- <img class="w-9 mr-6" draggable="false" src="/t-info.svg" alt="checkmark info" /> -->
<svg class="w-11 mr-6" viewBox="0 0 32.79 32.79">
+2 -2
View File
@@ -10,7 +10,7 @@
</script>
<div
class="rounded-full bg-gray-600 text-white w-4 h-4 z-30 text-center ml-2 relative cursor-pointer hover:bg-gray-500"
class="rounded-full bg-gray-600 dark:bg-gray-500 text-white w-4 h-4 z-30 text-center ml-2 relative cursor-pointer hover:bg-gray-500 dark:hover:bg-gray-400 transition-colors duration-200"
role="tooltip"
aria-describedby={tooltipId}
on:mouseenter={(e) => {
@@ -27,7 +27,7 @@
{#if showToolTip}
<div
id={tooltipId}
class="bg-gray-600 text-white w-max mt-2 px-2 py-2 rounded-md shadow-xl z-40"
class="bg-gray-600 dark:bg-gray-700 text-white w-max mt-2 px-2 py-2 rounded-md shadow-xl dark:shadow-gray-900/70 z-40 transition-colors duration-200"
style={tooltipStyle}
>
<p><slot /></p>
@@ -374,8 +374,10 @@
let isDetailsVisible = $$slots.default;
</script>
<div class="w-80vw z-[9000] col-start-1 col-end-4 flex flex-col">
<div class="">
<div
class="w-80vw z-[9000] col-start-1 col-end-4 flex flex-col bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 transition-colors duration-200"
>
<div class="bg-white dark:bg-gray-800 transition-colors duration-200">
<div class="mt-4 flex items-center flex-wrap">
<!-- mode tabs -->
<div class="flex">
@@ -385,9 +387,10 @@
isDetailsVisible = true;
}}
type="button"
class="h-8 border-2 rounded-md w-36 text-center cursor-pointer hover:opacity-80 flex items-center justify-center gap-2 mb-2"
class="h-8 border-2 border-gray-300 dark:border-gray-600 rounded-md w-36 text-center cursor-pointer hover:opacity-80 flex items-center justify-center gap-2 mb-2 text-gray-700 dark:text-gray-200 bg-white dark:bg-gray-700 transition-colors duration-200"
class:font-bold={isDetailsVisible}
class:bg-cta-blue={isDetailsVisible}
class:dark:bg-indigo-600={isDetailsVisible}
class:text-white={isDetailsVisible}
>
<svg
@@ -410,9 +413,10 @@
isDetailsVisible = false;
}}
type="button"
class="h-8 border-2 rounded-md w-36 text-center cursor-pointer hover:opacity-80 ml-1 flex items-center justify-center gap-2"
class="h-8 border-2 border-gray-300 dark:border-gray-600 rounded-md w-36 text-center cursor-pointer hover:opacity-80 ml-1 flex items-center justify-center gap-2 text-gray-700 dark:text-gray-200 bg-white dark:bg-gray-700 transition-colors duration-200"
class:font-bold={!isDetailsVisible}
class:bg-cta-blue={!isDetailsVisible}
class:dark:bg-indigo-600={!isDetailsVisible}
class:text-white={!isDetailsVisible}
>
<svg
@@ -443,7 +447,7 @@
<button
type="button"
on:click={triggerFileInput}
class="h-8 border-2 rounded-md px-3 text-center cursor-pointer hover:opacity-80 flex items-center justify-center gap-2"
class="h-8 border-2 border-gray-300 dark:border-gray-600 rounded-md px-3 text-center cursor-pointer hover:opacity-80 flex items-center justify-center gap-2 bg-white dark:bg-gray-700 text-gray-700 dark:text-gray-200 transition-colors duration-200"
>
<svg
xmlns="http://www.w3.org/2000/svg"
@@ -476,9 +480,10 @@
updatePreview();
}
}}
class="h-8 border-2 rounded-md px-3 text-center cursor-pointer hover:opacity-80 flex items-center justify-center gap-2"
class="h-8 border-2 border-gray-300 dark:border-gray-600 rounded-md px-3 text-center cursor-pointer hover:opacity-80 flex items-center justify-center gap-2 bg-white dark:bg-gray-700 text-gray-700 dark:text-gray-200 transition-colors duration-200"
class:font-bold={isPreviewVisible}
class:bg-cta-blue={isPreviewVisible}
class:dark:bg-indigo-600={isPreviewVisible}
class:text-white={isPreviewVisible}
>
<svg
@@ -499,7 +504,7 @@
<!-- template selector -->
<select
class="h-8 border-2 rounded-md px-3 bg-white text-black cursor-pointer"
class="h-8 border-2 border-gray-300 dark:border-gray-600 rounded-md px-3 bg-white dark:bg-gray-700 text-black dark:text-gray-200 cursor-pointer transition-colors duration-200"
on:change={(e) => {
const t = /** @type {HTMLSelectElement} */ (e.target);
if (t.value) {
@@ -524,7 +529,7 @@
id="domain-select"
bind:value={selectedDomain}
on:change={selectPreviewDomain}
class="h-8 border-2 rounded-md px-3 bg-white text-black cursor-pointer"
class="h-8 border-2 border-gray-300 dark:border-gray-600 rounded-md px-3 bg-white dark:bg-gray-700 text-black dark:text-gray-200 cursor-pointer transition-colors duration-200"
>
<option value="">Select preview domain...</option>
{#each domainMap.values() as domain}
@@ -537,7 +542,7 @@
<button
type="button"
on:click={openFullPagePreview}
class="h-8 border-2 rounded-md px-3 text-center cursor-pointer hover:opacity-80 flex items-center justify-center gap-2"
class="h-8 border-2 border-gray-300 dark:border-gray-600 rounded-md px-3 text-center cursor-pointer hover:opacity-80 flex items-center justify-center gap-2 bg-white dark:bg-gray-700 text-gray-700 dark:text-gray-200 transition-colors duration-200"
>
<svg
xmlns="http://www.w3.org/2000/svg"
@@ -563,8 +568,7 @@
<!-- details -->
{#if $$slots.default}
<div
class="flex flex-col lg:flex-row lg:items-center h-auto w-full justify-between mb-4"
class:lg:h-28={isDetailsVisible}
class="flex flex-col lg:flex-row lg:items-center h-auto w-full justify-between mb-4 bg-white dark:bg-gray-800 transition-colors duration-200"
>
{#if isDetailsVisible}
<slot />
@@ -575,21 +579,30 @@
<div class="flex h-full">
<div
class="flex flex-col border-2 border-black {!isPreviewVisible ? 'w-80vw' : 'w-1/2'}"
class="flex flex-col border-2 border-black dark:border-gray-600 bg-white dark:bg-gray-900 {!isPreviewVisible
? 'w-80vw'
: 'w-1/2'} transition-colors duration-200"
class:h-55vh={isDetailsVisible}
class:h-67vh={!isDetailsVisible}
>
<div id="monaco-editor" class="h-full" />
</div>
<div class="bg-cta-blue cursor-move w-1" class:hidden={!isPreviewVisible}>&nbsp;</div>
<div
class="bg-cta-blue dark:bg-indigo-600 cursor-move w-1 transition-colors duration-200"
class:hidden={!isPreviewVisible}
>
&nbsp;
</div>
{#if isPreviewVisible}
<div class="w-1/2 border-2 border-black">
<div
class="w-1/2 border-2 border-black dark:border-gray-600 bg-white transition-colors duration-200"
>
<iframe
bind:this={previewFrame}
sandbox="allow-forms allow-modals allow-popups allow-scripts allow-pointer-lock"
title="preview"
class="h-full w-full"
style="color-scheme: normal"
style="color-scheme: light;"
/>
</div>
{/if}
@@ -11,6 +11,7 @@
let editor = null;
let editorContainer = null;
let isDark = false;
const heightClasses = {
small: 'h-64',
@@ -18,7 +19,38 @@
large: 'h-96'
};
// Check for dark mode
const checkDarkMode = () => {
if (typeof window !== 'undefined') {
isDark = document.documentElement.classList.contains('dark');
}
};
onMount(() => {
checkDarkMode();
// Watch for dark mode changes
const observer = new MutationObserver(() => {
const newIsDark = document.documentElement.classList.contains('dark');
if (newIsDark !== isDark) {
isDark = newIsDark;
if (editor) {
monaco.editor.setTheme(isDark ? 'vs-dark' : 'vs-light');
}
}
});
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ['class']
});
const cleanup = () => {
observer.disconnect();
if (editor) {
editor.dispose();
}
};
self.MonacoEnvironment = {
getWorker: function (_, label) {
if (label === 'json') {
@@ -31,7 +63,7 @@
editor = monaco.editor.create(editorContainer, {
value: value || '',
language: language,
theme: 'vs-dark',
theme: isDark ? 'vs-dark' : 'vs-light',
automaticLayout: true,
minimap: {
enabled: false
@@ -60,11 +92,7 @@
value = editor.getValue();
});
return () => {
if (editor) {
editor.dispose();
}
};
return cleanup;
});
// Watch for external value changes
@@ -86,30 +114,38 @@
<div class="w-full">
<div
bind:this={editorContainer}
class="border border-gray-300 rounded-md {heightClasses[height]} w-full"
class="border border-gray-300 dark:border-gray-600 rounded-md {heightClasses[
height
]} w-full transition-colors duration-200"
></div>
{#if placeholder}
<div class="mt-2">
<button
type="button"
on:click={() => (showExample = !showExample)}
class="text-xs text-blue-600 hover:text-blue-800 underline focus:outline-none"
class="text-xs text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300 underline focus:outline-none transition-colors duration-200"
>
{showExample ? 'Hide' : 'Show'} example
</button>
{#if showExample}
<div class="mt-2 p-3 bg-gray-50 border border-gray-200 rounded-md">
<div
class="mt-2 p-3 bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-600 rounded-md transition-colors duration-200"
>
<div class="flex justify-between items-start mb-2">
<span class="text-xs font-medium text-gray-700">Example:</span>
<span
class="text-xs font-medium text-gray-700 dark:text-gray-300 transition-colors duration-200"
>Example:</span
>
<button
type="button"
on:click={loadExample}
class="text-xs text-blue-600 hover:text-blue-800 underline"
class="text-xs text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300 underline transition-colors duration-200"
>
Load example
</button>
</div>
<pre class="text-xs text-gray-600 whitespace-pre-wrap">{placeholder}</pre>
<pre
class="text-xs text-gray-600 dark:text-gray-300 whitespace-pre-wrap transition-colors duration-200">{placeholder}</pre>
</div>
{/if}
</div>
@@ -92,19 +92,19 @@
<div class="flex">
<nav
class="hidden lg:flex flex-col transition-all fixed top-16 z-10 bg-gradient-to-b from-pc-darkblue to-indigo-400 rounded-br-lg overflow-y-auto overflow-x-hidden min-h-0 max-h-[calc(100vh-4rem)] box-content border-r-[1px] border-pc-darkblue"
class="hidden lg:flex flex-col transition-all fixed top-16 z-10 bg-gradient-to-b from-pc-darkblue to-indigo-400 dark:from-gray-900 dark:to-gray-800 rounded-br-lg overflow-y-auto overflow-x-hidden min-h-0 max-h-[calc(100vh-4rem)] box-content border-r-[1px] border-pc-darkblue dark:border-gray-700"
class:w-40={isExpanded}
class:w-12={!isExpanded}
>
<div
class="sticky top-0 bg-highlight-blue/20 border-b w-full border-blue-700/30 transform-none"
class="sticky top-0 bg-highlight-blue/20 dark:bg-gray-800/70 border-b w-full border-blue-700/30 dark:border-gray-600 transform-none transition-colors duration-200"
>
<button
class="w-full flex items-center justify-center rounded-md hover:bg-blue-600/30 transition-colors group px-3 py-2"
class="w-full flex items-center justify-center rounded-md hover:bg-blue-600/30 dark:hover:bg-gray-700/70 transition-colors group px-3 py-2"
on:click={() => (isExpanded = !isExpanded)}
>
<svg
class="text-blue-100 duration-200 w-6"
class="text-blue-100 dark:text-gray-100 duration-200 w-6 transition-colors"
class:rotate-180={!isExpanded}
xmlns="http://www.w3.org/2000/svg"
fill="none"
@@ -123,13 +123,15 @@
<!-- Navigation Items -->
<div
class="flex flex-col py-4 flex-1 overflow-y-auto {scrollBarClassesVertical} [&::-webkit-scrollbar-track]:bg-cta-blue"
class="flex flex-col py-4 flex-1 overflow-y-auto {scrollBarClassesVertical} [&::-webkit-scrollbar-track]:bg-cta-blue dark:[&::-webkit-scrollbar-track]:bg-gray-800"
>
{#each menu as link}
{#if link.type === 'submenu'}
<div class="py-1 mt-4 first:mt-0">
{#if isExpanded}
<div class="px-3 py-2 text-xs font-semibold text-blue-100 uppercase tracking-wider">
<div
class="px-3 py-2 text-xs font-semibold text-blue-100 dark:text-gray-200 uppercase tracking-wider transition-colors duration-200"
>
{link.label}
</div>
{/if}
@@ -139,8 +141,8 @@
<a
class="flex items-center px-3 py-2 text-sm transition-all duration-150 relative group
{$page.url.pathname === item.route
? 'text-white font-medium bg-active-blue shadow-md'
: 'text-blue-100 hover:shadow-md hover:bg-highlight-blue hover:text-white'}"
? 'text-white font-medium bg-active-blue dark:bg-indigo-600 shadow-md'
: 'text-blue-100 dark:text-gray-200 hover:shadow-md hover:bg-highlight-blue dark:hover:bg-gray-700 hover:text-white dark:hover:text-gray-100'}"
class:hidden={shouldHideMenuItem(item.route)}
draggable="false"
href={item.route}
@@ -164,7 +166,7 @@
{/if}
{#if $page.url.pathname === item.route}
<div class="absolute left-0 top-0 bottom-0 w-1 bg-white"></div>
<div class="absolute left-0 top-0 bottom-0 w-1 bg-white dark:bg-blue-400"></div>
{/if}
</a>
{/each}
@@ -174,8 +176,8 @@
<a
class="flex items-center px-3 py-2 text-sm transition-all duration-150 relative group
{$page.url.pathname === link.route
? 'text-white font-medium bg-active-blue shadow-md'
: 'text-blue-100 hover:text-white'}"
? 'text-white font-medium bg-active-blue dark:bg-indigo-600 shadow-md'
: 'text-blue-100 dark:text-gray-200 hover:text-white dark:hover:text-gray-100 dark:hover:bg-gray-700'}"
draggable="false"
href={link.route}
>
@@ -188,16 +190,16 @@
<span class="ml-3 truncate">{link.label}</span>
{:else}
<div
class="absolute left-14 rounded bg-gray-900 text-white px-2 py-1 ml-6 text-sm
invisible opacity-0 -translate-x-3 group-hover:visible group-hover:opacity-100 group-hover:translate-x-0
transition-all duration-150 whitespace-nowrap z-50 shadow-lg"
class="absolute left-14 rounded bg-gray-900 dark:bg-gray-800 text-white dark:text-gray-100 px-2 py-1 ml-6 text-sm
invisible opacity-0 -translate-x-3 group-hover:visible group-hover:opacity-100 group-hover:translate-x-0
transition-all duration-150 whitespace-nowrap z-50 shadow-lg border dark:border-gray-600"
>
{link.label}
</div>
{/if}
{#if $page.url.pathname === link.route}
<div class="absolute left-0 top-0 bottom-0 w-1 bg-white"></div>
<div class="absolute left-0 top-0 bottom-0 w-1 bg-white dark:bg-blue-400"></div>
{/if}
</a>
{/if}
@@ -2,6 +2,7 @@
import { AppStateService } from '$lib/service/appState';
import { onMount } from 'svelte';
import Logo from './Logo.svelte';
import ThemeToggle from '../ThemeToggle.svelte';
const appState = AppStateService.instance;
@@ -112,17 +113,19 @@
$: initials = getInitials(username || 'U');
</script>
<div class="sticky top-0 z-20 col-span-12 h-16 bg-pc-darkblue flex justify-between items-center">
<div
class="header-container sticky top-0 z-20 col-span-12 h-16 bg-pc-darkblue dark:bg-gray-800 border-b border-pc-darkblue/20 dark:border-gray-700 flex justify-between items-center"
>
<Logo />
{#if isInstalled}
<div class="hidden lg:flex flex-row items-center px-8 h-full justify-self-end">
{#if context.current === AppStateService.CONTEXT.COMPANY}
<p class="text-slate-300 uppercase font-bold text-lg mr-4">
<p class="text-slate-300 dark:text-gray-300 uppercase font-bold text-lg mr-4">
{context.companyName}
</p>
{/if}
<button
class="rounded-md h-3/4 px-8 text-white bg-indigo-500 hover:bg-cta-blue uppercase font-semibold mr-4"
class="rounded-md h-3/4 px-8 text-white bg-indigo-500 hover:bg-cta-blue dark:bg-indigo-600 dark:hover:bg-indigo-700 uppercase font-semibold mr-4 transition-colors duration-200"
on:click={toggleChangeCompanyModal}
>
Change company
@@ -130,14 +133,15 @@
{#if isUpdateAvailable}
<a
class="flex items-center gap-2 mr-8 text-lg font-medium text-white bg-gradient-to-r from-indigo-500 to-purple-500 rounded-md px-4 py-2 transition-all duration-300 transform hover:-translate-y-0.5 focus:outline-none focus:ring-2 focus:ring-indigo-400 focus:ring-offset-2 active:scale-95 fixed bottom-4 right-2 shadow-md shadow-black"
class="flex items-center gap-2 mr-8 text-lg font-medium text-white bg-gradient-to-r from-indigo-500 to-purple-500 dark:from-indigo-600 dark:to-purple-600 rounded-md px-4 py-2 transition-all duration-300 transform hover:-translate-y-0.5 focus:outline-none focus:ring-2 focus:ring-indigo-400 focus:ring-offset-2 dark:focus:ring-offset-gray-800 active:scale-95 fixed bottom-4 right-2 shadow-md shadow-black dark:shadow-gray-900"
href={'/settings/update'}
>
<span class=""></span>
<span>Update Available</span>
</a>
{/if}
<div class="relative ml-10 flex items-center">
<div class="relative ml-10 flex items-center gap-4">
<ThemeToggle />
<button
id="toggle-profile-menu"
class="group flex items-center"
@@ -145,7 +149,7 @@
>
<!-- Main Circle with Initials -->
<div
class="w-10 h-10 rounded-full bg-cta-blue hover:bg-indigo-500 flex items-center justify-center text-white font-medium relative"
class="w-10 h-10 rounded-full bg-cta-blue hover:bg-indigo-500 dark:bg-indigo-600 dark:hover:bg-indigo-700 flex items-center justify-center text-white font-medium relative transition-colors duration-200"
>
{initials}
@@ -154,7 +158,7 @@
<!-- Dropdown Indicator -->
<svg
class="w-4 h-4 ml-2 text-gray-300 transition-transform duration-200 group-hover:text-white"
class="w-4 h-4 ml-2 text-gray-300 dark:text-gray-400 transition-transform duration-200 group-hover:text-white"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
@@ -170,25 +174,40 @@
</div>
</div>
<div class="flex lg:hidden items-center mr-4">
<div class="flex lg:hidden items-center mr-4 gap-4">
<div
class="flex items-center justify-center w-10 h-10 rounded-lg hover:bg-white/10 transition-colors duration-200"
>
<ThemeToggle />
</div>
<button
class="rounded-md px-3 py-1 text-white bg-indigo-500 hover:bg-cta-blue uppercase font-semibold text-xs mr-3"
class="rounded-md px-3 py-2 text-white bg-indigo-500 hover:bg-cta-blue dark:bg-indigo-600 dark:hover:bg-indigo-700 uppercase font-semibold text-xs transition-colors duration-200"
on:click={toggleChangeCompanyModal}
>
Change company
</button>
<button class="flex w-14" on:click={() => (isMobileMenuVisible = !isMobileMenuVisible)}>
<img class="" src="/mob-menu-button.svg" alt="toggle mobile menu" />
<button
class="flex items-center justify-center w-10 h-10 rounded-lg hover:bg-white/10 transition-colors duration-200"
on:click={() => (isMobileMenuVisible = !isMobileMenuVisible)}
>
<img class="w-6 h-6" src="/mob-menu-button.svg" alt="toggle mobile menu" />
</button>
</div>
{/if}
</div>
<style>
button {
filter: contrast(1.1) saturate(1.2);
/* Prevent any hover effects on the header */
.header-container {
background-color: #0b2063 !important;
}
button:hover {
filter: contrast(1.2) saturate(1.3);
.header-container:hover {
background-color: #0b2063 !important;
}
:global(.dark) .header-container {
background-color: #1f2937 !important;
}
:global(.dark) .header-container:hover {
background-color: #1f2937 !important;
}
</style>
+16 -1
View File
@@ -7,7 +7,22 @@
on:keydown={(e) => e.key === 'Enter' && goto('/dashboard/')}
tabindex="0"
role="button"
class="flex items-center w-40 sm:w-40 md:w-42 lg:w-56 justify-center py-4 my-6 rounded-md ml-4"
class="flex items-center w-40 sm:w-40 md:w-42 lg:w-56 justify-center py-4 my-6 ml-4 cursor-pointer"
>
<img draggable="false" src="/logo-white.svg" alt="logo" />
</div>
<style>
div {
background: none !important;
}
div:hover {
background: none !important;
background-color: transparent !important;
}
div:focus {
background: none !important;
background-color: transparent !important;
outline: none !important;
}
</style>
@@ -5,10 +5,13 @@
</script>
<a
class="py-2 px-4 text-white hover:bg-active-blue hover:text-white hover:rounded-md"
class:bg-highlight-blue={$page.url.pathname === href}
class:font-semibold={$page.url.pathname === href}
class:rounded-md={$page.url.pathname === href}
class="pl-5 py-2 text-white last:rounded-md first:rounded-t-md transition-colors duration-200"
class:hover:shadow-md={$page.url.pathname !== href}
class:hover:bg-highlight-blue={$page.url.pathname !== href}
class:dark:hover:bg-gray-600={$page.url.pathname !== href}
class:bg-active-blue={$page.url.pathname === href}
class:dark:bg-gray-700={$page.url.pathname === href}
class:shadow-md={$page.url.pathname === href}
class:hidden
{href}
on:click
@@ -3,6 +3,7 @@
import { menu, mobileTopMenu } from '$lib/consts/navigation';
import MenuLink from './MenuLink.svelte';
import { shouldHideMenuItem } from '$lib/utils/common';
import ThemeToggle from '../ThemeToggle.svelte';
export let visible = false;
export let username = '';
@@ -10,48 +11,91 @@
</script>
{#if visible}
<div class="fixed top-0 left-0 w-full h-full bg-pc-darkblue z-40 overflow-y-auto pb-4">
<div class="flex justify-between h-16">
<img class="w-40 sm:w-40 md:w-42 lg:w-56 ml-4" src="/logo-white.svg" alt="logo" />
<button class="mr-4 w-14" on:click={() => (visible = !visible)}>
<img class="w-3/4" src="/mob-menu-close.svg" alt="close mobile menu" />
</button>
</div>
<div>
<div class="flex flex-col px-4 py-4 rounded-b-xl">
<div class="flex py-6 border-b-2 border-white mb-4">
<!-- <div class="bg-slate-50 w-16 h-16 rounded-full" /> -->
<div>
<h1 class="font-bold text-3xl ml-6 text-white">{username ?? ''}</h1>
<button
on:click={onClickLogout}
class="bg-cta-blue hover:bg-pc-lightblue uppercase font-bold ml-6 mt-2 py text-white rounded-md"
>
<p class="py px-8">Log Out</p>
</button>
</div>
</div>
<div class="flex flex-col text-white">
{#each mobileTopMenu as link}
<a
class="pl-5 py-2 hover:bg-cta-blue hover:text-white rounded-md"
class:bg-gray-600={$page.url.pathname === link.route}
class:hidden={shouldHideMenuItem(link.route)}
on:click={() => (visible = !visible)}
target={link.external ? '_blank' : '_self'}
href={link.route}>{link.label}</a
>
{/each}
<!-- Overlay -->
<button
class="fixed inset-0 bg-black bg-opacity-50 z-40 cursor-default"
on:click={() => (visible = false)}
aria-label="Close mobile menu"
></button>
<!-- Mobile Menu -->
<div
class="mobile-menu-content fixed top-0 left-0 w-full h-full bg-pc-darkblue dark:bg-gray-900 z-50 overflow-y-auto shadow-xl transition-colors duration-200"
>
<!-- Header -->
<div
class="mobile-menu-header flex justify-between h-20 items-center bg-pc-darkblue dark:bg-gray-800 px-6"
>
<img class="w-40 h-auto" src="/logo-white.svg" alt="logo" />
<div class="flex items-center gap-4">
<div
class="flex items-center justify-center w-12 h-12 rounded-lg hover:bg-white/10 dark:hover:bg-gray-600/30 transition-colors duration-200"
>
<ThemeToggle />
</div>
<button
class="flex items-center justify-center w-12 h-12 rounded-lg hover:bg-white/10 dark:hover:bg-gray-600/30 transition-colors duration-200"
on:click={() => (visible = false)}
>
<img class="w-6 h-6" src="/mob-menu-close.svg" alt="close mobile menu" />
</button>
</div>
</div>
<div>
<div class="flex flex-col bg-cta-blue px-4 pt-4">
<!-- User Section -->
<div class="p-6 border-b border-white dark:border-gray-700">
<h1 class="font-bold text-xl text-white dark:text-gray-100 mb-4">
{username ?? ''}
</h1>
<button
on:click={onClickLogout}
class="bg-cta-blue dark:bg-indigo-600 dark:hover:bg-indigo-700 uppercase font-bold py-3 px-6 rounded-md transition-colors duration-200 text-sm text-white"
>
Log Out
</button>
</div>
<!-- Top Menu -->
<div class="p-4">
<div
class="bg-gradient-to-b from-cta-blue to-indigo-500 dark:from-gray-800 dark:to-gray-700 rounded-md"
>
{#each mobileTopMenu as link}
<a
class="block text-center py-4 text-white text-lg font-medium first:rounded-t-md last:rounded-b-md transition-colors duration-200"
class:bg-active-blue={$page.url.pathname === link.route}
class:dark:bg-gray-700={$page.url.pathname === link.route}
class:shadow-md={$page.url.pathname === link.route}
class:hidden={shouldHideMenuItem(link.route)}
on:click={() => (visible = false)}
target={link.external ? '_blank' : '_self'}
href={link.route}
>
{link.label}
</a>
{/each}
</div>
</div>
<!-- Main Menu -->
<div class="p-4 pt-0">
<div
class="bg-gradient-to-b from-cta-blue to-indigo-500 dark:from-gray-800 dark:to-gray-700 rounded-md"
>
{#each menu as link}
{#if link.type === 'submenu'}
<div class="text-white font-semibold text-xl">{link.label}</div>
<div
class="text-center py-4 text-white font-semibold text-lg border-b border-white/20 dark:border-gray-600"
>
{link.label}
</div>
{#each link.items as item, i (i)}
<MenuLink href={item.route} on:click={() => (visible = !visible)}>
<a
class="block text-center py-3 text-white text-base transition-colors duration-200"
class:last:rounded-b-md={i === link.items.length - 1}
href={item.route}
on:click={() => (visible = false)}
>
{#if i === 0}
Overview
{:else if item.singleLabel}
@@ -59,13 +103,49 @@
{:else}
{item.label}
{/if}
</MenuLink>
</a>
{/each}
{:else}
<MenuLink href={link.route}>{link.label}</MenuLink>
<a
class="block text-center py-4 text-white text-lg font-medium transition-colors duration-200"
href={link.route}
on:click={() => (visible = false)}
>
{link.label}
</a>
{/if}
{/each}
</div>
</div>
</div>
{/if}
<style>
/* Prevent any hover effects on the mobile menu header */
:global(.mobile-menu-header) {
background-color: #0b2063 !important;
}
:global(.mobile-menu-header:hover) {
background-color: #0b2063 !important;
}
:global(.dark .mobile-menu-header) {
background-color: #1f2937 !important;
}
:global(.dark .mobile-menu-header:hover) {
background-color: #1f2937 !important;
}
/* Prevent any hover effects on the mobile menu content */
.mobile-menu-content {
background-color: #0b2063 !important;
}
.mobile-menu-content:hover {
background-color: #0b2063 !important;
}
:global(.dark) .mobile-menu-content {
background-color: #111827 !important;
}
:global(.dark) .mobile-menu-content:hover {
background-color: #111827 !important;
}
</style>
@@ -26,6 +26,14 @@
document.removeEventListener('click', handleClickOutsideNavigation);
}
}
// Custom transition that only fades in
function fadeIn(node, { duration = 150 }) {
return {
duration,
css: (t) => `opacity: ${t}`
};
}
</script>
{#if visible}
@@ -34,15 +42,17 @@
class="lg:flex flex-col h-fit lg:col-start-10 lg:col-span-3 row-start-1 xl:col-start-11 xl:col-span-2 2xl:col-start-11 2xl:col-span-2 sticky top-20 z-30"
>
<div
class="flex flex-col bg-gradient-to-b from-cta-blue to-indigo-500 rounded-md"
transition:fade={{ duration: 150 }}
class="flex flex-col bg-gradient-to-b from-cta-blue to-indigo-500 dark:from-gray-800 dark:to-gray-700 rounded-md transition-colors duration-200"
in:fadeIn={{ duration: 150 }}
>
{#each topMenu as item}
<a
class="pl-5 py-2 text-white last:rounded-md first:rounded-t-md"
class="pl-5 py-2 text-white last:rounded-md first:rounded-t-md transition-colors duration-200"
class:hover:shadow-md={$page.url.pathname !== item.route}
class:hover:bg-highlight-blue={$page.url.pathname !== item.route}
class:dark:hover:bg-gray-600={$page.url.pathname !== item.route}
class:bg-active-blue={$page.url.pathname === item.route}
class:dark:bg-gray-700={$page.url.pathname === item.route}
class:shadow-md={$page.url.pathname === item.route}
class:hidden={shouldHideMenuItem(item.route)}
target={item.external ? '_blank' : '_self'}
@@ -55,9 +65,11 @@
{/each}
<button
on:click={logout}
class="bg-white uppercase font-bold hover:bg-pc-lightblue py-2 mx-4 my-4 rounded-md"
class="bg-white dark:bg-gray-800 uppercase font-bold hover:bg-pc-lightblue dark:hover:bg-gray-700 py-2 mx-4 my-4 rounded-md transition-colors duration-200"
>
<p class="text-cta-blue py px-8">Log Out</p>
<p class="text-cta-blue dark:text-gray-100 py px-8 transition-colors duration-200">
Log Out
</p>
</button>
</div>
</nav>
@@ -117,17 +117,21 @@
<div class="flex-grow p-6">
{#if isLoadingCompanies}
<div class="flex items-center justify-center py-8">
<div class="text-gray-500">Loading companies...</div>
<div class="text-gray-500 dark:text-gray-400 transition-colors duration-200">
Loading companies...
</div>
</div>
{:else if companies.length === 0}
<div class="flex flex-col items-center justify-center py-8 text-center">
<div class="text-gray-500 mb-4">No companies found.</div>
<div class="text-sm text-gray-400 mb-4">
<div class="text-gray-500 dark:text-gray-400 mb-4 transition-colors duration-200">
No companies found.
</div>
<div class="text-sm text-gray-400 dark:text-gray-500 mb-4 transition-colors duration-200">
You need to create a company first before you can switch to it.
</div>
<a
href="/company/"
class="bg-cta-blue hover:bg-blue-700 text-sm uppercase font-bold px-4 py-2 text-white rounded-md"
class="bg-cta-blue dark:bg-blue-600 hover:bg-blue-700 dark:hover:bg-blue-700 text-sm uppercase font-bold px-4 py-2 text-white rounded-md transition-colors duration-200"
on:click={() => {
visible = false;
}}
@@ -143,11 +147,13 @@
</div>
<!-- Button Section -->
<div class="border-t p-6 mt-36 flex flex-wrap gap-4 justify-end">
<div
class="border-t border-gray-200 dark:border-gray-600 p-6 mt-36 flex flex-wrap gap-4 justify-end transition-colors duration-200"
>
{#if inContext}
<button
type="button"
class="bg-slate-400 hover:bg-slate-300 text-sm mr-2 uppercase font-bold px-4 py-2 text-white rounded-md disabled:opacity-50 disabled:cursor-not-allowed"
class="bg-slate-400 dark:bg-gray-600 hover:bg-slate-300 dark:hover:bg-gray-500 text-sm mr-2 uppercase font-bold px-4 py-2 text-white rounded-md disabled:opacity-50 disabled:cursor-not-allowed transition-colors duration-200"
disabled={isLoadingCompanies}
on:click={onClickSwitchToAdministratorContext}
>
@@ -157,7 +163,7 @@
<button
type="submit"
class="bg-cta-blue hover:bg-blue-700 text-sm uppercase font-bold px-4 py-2 text-white rounded-md disabled:opacity-50 disabled:cursor-not-allowed"
class="bg-cta-blue dark:bg-blue-600 hover:bg-blue-700 dark:hover:bg-blue-700 text-sm uppercase font-bold px-4 py-2 text-white rounded-md disabled:opacity-50 disabled:cursor-not-allowed transition-colors duration-200"
disabled={isLoadingCompanies || !selectedCompany}
on:click={onClickSwitch}
>
@@ -46,20 +46,20 @@
<!--
<h3 class="text-lg font-medium text-gray-900">Delete {type}</h3>
-->
<p class="mt-2 text-gray-600">
<p class="mt-2 text-gray-600 dark:text-gray-300">
Are you sure you want to delete
{#if name?.length > 30}
<br />
{/if}
<span class="font-medium text-gray-900">"{name}"</span>?
<span class="font-medium text-gray-900 dark:text-gray-100">"{name}"</span>?
</p>
</div>
<!-- Impact Section -->
{#if list.length}
<div class="bg-gray-50 rounded-lg p-4">
<p class="font-medium text-gray-900 mb-3">Side effects:</p>
<ul class="space-y-2 ml-4 list-disc text-gray-600">
<div class="bg-gray-50 dark:bg-gray-700 rounded-lg p-4 transition-colors duration-200">
<p class="font-medium text-gray-900 dark:text-gray-100 mb-3">Side effects:</p>
<ul class="space-y-2 ml-4 list-disc text-gray-600 dark:text-gray-300">
{#each list as line}
<li>{line}</li>
{/each}
@@ -85,7 +85,7 @@
{/if}
{#if permanent}
<p class="text-red-700 font-medium">This action cannot be undone.</p>
<p class="text-red-700 dark:text-red-400 font-medium">This action cannot be undone.</p>
{/if}
</div>
</Alert>
@@ -5,7 +5,7 @@
</script>
<button
class="hover:bg-gray-100 px-2 py-1 rounded-md transition-colors w-full text-left text-ellipsis overflow-hidden"
class="hover:bg-gray-100 dark:hover:bg-gray-700 px-2 py-1 rounded-md transition-colors w-full text-left text-ellipsis overflow-hidden text-gray-900 dark:text-gray-100"
title={text}
on:click={() => onClickCopy(text)}
>
@@ -5,12 +5,16 @@
export let colspan = 100;
</script>
<tr class="text-center bg-pleasant-gray">
<tr class="text-center bg-pleasant-gray dark:bg-gray-700 transition-colors duration-200">
<td class="p-24" {colspan}>
{#if page === 1}
<p class="text-lg text-gray-600">No {plural} found</p>
<p class="text-lg text-gray-600 dark:text-gray-300 transition-colors duration-200">
No {plural} found
</p>
{:else}
<p class="text-lg text-gray-600">No more results found</p>
<p class="text-lg text-gray-600 dark:text-gray-300 transition-colors duration-200">
No more results found
</p>
{/if}
</td>
</tr>
@@ -10,7 +10,9 @@
});
</script>
<div class="flex items-center text-ellipsis">
<div
class="flex items-center text-ellipsis text-gray-900 dark:text-gray-100 transition-colors duration-200"
>
<div class="w-4 h-4 {event.color} mr-2 rounded-sm"></div>
{event.name}
</div>
@@ -56,10 +56,13 @@
<div
bind:this={tableWrapper}
class="
border-2 rounded-md px-4 py-4 overflow-x-auto
border-2 border-gray-200 dark:border-gray-600 rounded-md px-4 py-4 overflow-x-auto bg-white dark:bg-gray-800 transition-colors duration-200
{scrollBarClassesHorizontal}"
>
<table class="w-full table-fixed" class:animate-pulse={isGhost}>
<table
class="w-full table-fixed bg-white dark:bg-gray-800 transition-colors duration-200"
class:animate-pulse={isGhost}
>
<TableHeader {isGhost} {columns} {sortable} {hasActions} {pagination} />
{#if !hasData && !isGhost}
<EmptyTableResult page={currentPage} {plural} colspan={columnsLength} />
@@ -11,7 +11,7 @@
</script>
<td
class={`pl-4 font-regular text-slate-600 text-ellipsis whitespace-nowrap overflow-hidden pr-4`}
class={`pl-4 font-regular text-slate-600 dark:text-gray-300 text-ellipsis whitespace-nowrap overflow-hidden pr-4 transition-colors duration-200`}
title={isDate ? '' : value}
>
{#if value}
@@ -1,5 +1,5 @@
<td class="pl-4 w-48 text-center border: hidden;">
<p class="font-regular text-slate-600">
<td class="w-48 text-center border: hidden;">
<p class="font-regular text-slate-600 dark:text-gray-300 transition-colors duration-200">
<slot />
</p>
</td>
@@ -3,7 +3,9 @@
</script>
<td class="pl-4 w-40 text-center border: hidden;">
<p class="font-regular text-slate-600 flex justify-center">
<p
class="font-regular text-slate-600 dark:text-gray-300 flex justify-center transition-colors duration-200"
>
{value ? 'Yes' : 'No'}
</p>
</td>
@@ -1,3 +1 @@
<td class="pl-4 w-4 border: hidden;">
</td>
<td class="pl-4 w-4 border: hidden; bg-white dark:bg-gray-800 transition-colors duration-200"> </td>
@@ -8,12 +8,16 @@
</script>
{#if disabled}
<button class="px py text-slate-300 cursor-not-allowed" {disabled} {title}>
<button
class="px py text-slate-300 dark:text-gray-500 cursor-not-allowed transition-colors duration-200"
{disabled}
{title}
>
<p class="ml-2 text-left">{name}</p>
</button>
{:else}
<button
class="px py-1 text-slate-600 hover:bg-red-400 hover:text-white cursor-pointer"
class="px py-1 text-slate-600 dark:text-gray-300 hover:bg-red-400 dark:hover:bg-red-500 hover:text-white cursor-pointer transition-colors duration-200"
on:click
{title}
>
@@ -16,12 +16,16 @@
</script>
{#if disabled}
<button class="px py-1 text-slate-300 cursor-not-allowed" {disabled} {title}>
<button
class="px py-1 text-slate-300 dark:text-gray-500 cursor-not-allowed transition-colors duration-200"
{disabled}
{title}
>
<p class="ml-2 text-left">{name}</p>
</button>
{:else}
<button
class="px py-1 text-slate-600 hover:bg-highlight-blue hover:text-white cursor-pointer"
class="px py-1 text-slate-600 dark:text-gray-300 hover:bg-highlight-blue dark:hover:bg-blue-600 hover:text-white cursor-pointer transition-colors duration-200"
on:click
on:keydown={handleKeydown}
{title}
@@ -128,16 +128,31 @@
<div class="">
<button
bind:this={buttonRef}
class="py-2 px-2"
class="w-full h-full py-3 flex items-center justify-center"
on:click|stopPropagation|preventDefault={toggle}
on:keydown={handleKeydown}
>
<svg width="3.335557" height="16.465519" viewBox="0 0 0.88253281 4.3565019">
<g transform="translate(-892.25669,88.863024)">
<g transform="matrix(0,1.0139418,-1.0139418,0,802.48114,-807.2715)">
<circle class="fill-cta-blue" cx="708.99603" cy="-88.976357" r="0.40846577" />
<circle class="fill-cta-blue" cx="710.67859" cy="-88.976357" r="0.40846577" />
<circle class="fill-cta-blue" cx="712.36115" cy="-88.976357" r="0.40846577" />
<circle
class="fill-cta-blue dark:fill-blue-500 transition-colors duration-200"
cx="708.99603"
cy="-88.976357"
r="0.40846577"
/>
<circle
class="fill-cta-blue dark:fill-blue-500 transition-colors duration-200"
cx="710.67859"
cy="-88.976357"
r="0.40846577"
/>
<circle
class="fill-cta-blue dark:fill-blue-500 transition-colors duration-200"
cx="712.36115"
cy="-88.976357"
r="0.40846577"
/>
</g>
</g>
</svg>
@@ -145,7 +160,7 @@
<div
bind:this={menuRef}
class="absolute bg-white drop-shadow-md z-20 w-48 rounded-md overflow-y-scroll {scrollBarClassesVertical}"
class="absolute bg-white dark:bg-gray-800 drop-shadow-md dark:shadow-gray-900/50 border dark:border-gray-600 z-20 w-48 rounded-md overflow-y-scroll transition-colors duration-200 {scrollBarClassesVertical}"
class:hidden={!isMenuVisible}
>
<ul class="flex flex-col text-left">
@@ -1,3 +1,3 @@
<thead class="table-header-group">
<slot />
<thead class="table-header-group bg-white dark:bg-gray-800 transition-colors duration-200">
<slot />
</thead>
@@ -34,7 +34,7 @@
</script>
<th
class="pl-4 bg-grayblue-light py-4 border-hidden first:rounded-tl-lg first:rounded-bl-lg last:rounded-tr-lg last:border-4 min-w-48"
class="pl-4 bg-grayblue-light dark:bg-gray-700 py-4 border-hidden first:rounded-tl-lg first:rounded-bl-lg last:rounded-tr-lg last:border-4 min-w-48 transition-colors duration-200"
class:rounded-br-lg={last}
class:rounded-tr-lg={last}
class:w-48={size === 'small'}
@@ -49,7 +49,9 @@
on:click|preventDefault={setSortAndSortBy}
>
<div class="w-full">
<p class="font-bold text-slate-600 text-{alignText} flex">
<p
class="font-bold text-slate-600 dark:text-gray-200 text-{alignText} flex transition-colors duration-200"
>
{#if !isGhost}
{title.length ? title : column}
{:else}
@@ -58,7 +60,7 @@
{#if sortable && column.toLowerCase() === sortBy.toLowerCase() && !isGhost}
<div
class:bg-transparent={sortOrder === ''}
class="flex justify-center items-center w-6 h-6 ml-2 rounded-md bg-cta-blue"
class="flex justify-center items-center w-6 h-6 ml-2 rounded-md bg-cta-blue dark:bg-blue-600 transition-colors duration-200"
>
{#if sortOrder === 'asc'}
<div>
@@ -1,5 +1,7 @@
<th class="bg-cta-light-blue w-20 py-4 border-hidden rounded-lg">
<p class="font-bold text-slate-600 text-center">
<th
class="bg-cta-light-blue dark:bg-gray-700 w-20 py-4 border-hidden rounded-lg transition-colors duration-200"
>
<p class="font-bold text-slate-600 dark:text-gray-200 text-center transition-colors duration-200">
<slot />
</p>
</th>
@@ -1,5 +1,9 @@
<th class="pl-4 bg-grayblue-light w-48 py-4 border-hidden">
<p class="font-bold text-slate-600 flex justify-center text-center">
<th
class="pl-4 bg-grayblue-light dark:bg-gray-700 w-48 py-4 border-hidden transition-colors duration-200"
>
<p
class="font-bold text-slate-600 dark:text-gray-200 flex justify-center text-center transition-colors duration-200"
>
<slot />
</p>
</th>
@@ -1,3 +1,2 @@
<th class="pl-4 bg-white w-4 py-4 border-hidden">
<th class="pl-4 bg-white dark:bg-gray-800 w-4 py-4 border-hidden transition-colors duration-200">
</th>
@@ -1,5 +1,7 @@
<th class="pl-4 bg-grayblue-light w-48 py-4 border-hidden rounded-md">
<p class="font-bold text-slate-600 text-left">
<th
class="pl-4 bg-grayblue-light dark:bg-gray-700 w-48 py-4 border-hidden rounded-md transition-colors duration-200"
>
<p class="font-bold text-slate-600 dark:text-gray-200 text-left transition-colors duration-200">
<slot />
</p>
</th>
@@ -1,3 +1,5 @@
<tr class="table-row odd:bg-white even:bg-pleasant-gray h-10 hover:bg-cta-light-blue">
<tr
class="table-row odd:bg-white odd:dark:bg-gray-800 even:bg-pleasant-gray even:dark:bg-gray-900 h-10 hover:bg-cta-light-blue hover:dark:bg-indigo-800 transition-colors duration-200"
>
<slot />
</tr>
@@ -1,3 +1,3 @@
<tr class="table-row bg-white h-4">
<tr class="table-row bg-white dark:bg-gray-800 h-4 transition-colors duration-200">
<slot />
</tr>
+58 -54
View File
@@ -1,104 +1,108 @@
import { writable } from 'svelte/store';
import { browser } from '$app/environment';
// modeLight is the state for light mode
// mode constants
export const modeLight = 'light';
// modeDark is the state for dark mode
export const modeDark = 'dark';
// modeNotSet is the state for when the mode is not set
const modeNotSet = 'not-set';
// modeKey is the key used to store the mode in localstorage
const modeKey = 'theme-mode';
/**
* theme is either 'light' or 'dark' -
* theme store - either 'light' or 'dark'
* use it to know which theme is current and subscribe to know when it changes
*/
export const theme = writable('dark');
export const theme = writable(modeLight);
/**
* toggleMode is used to toggle the theme mode from light to dark or vice versa
* it is used by the toggle button in the header
* @returns {void} - the new mode, either light or dark
* toggle the theme mode from light to dark or vice versa
* @returns {void}
*/
export const toggleMode = () => {
theme.update((current) => {
let mode = current;
switch (mode) {
case modeLight:
mode = modeDark;
break;
case modeDark:
mode = modeLight;
break;
default:
throw new Error(`Unknown mode passed to changeMode: ${mode} - must be 'light' or 'dark'`);
}
setModeToLocalStorage(mode);
return mode;
const newMode = current === modeLight ? modeDark : modeLight;
setModeToLocalStorage(newMode);
applyThemeToHTML(newMode);
return newMode;
});
};
/**
* setupTheme is used to set the theme mode
* it must be called once it the bootstrapping of the application
* it checks if a prefered mode is set in localstorage, if not
* it checks the OS preferred mode and if that is not set it defaults to light
* the default mode is saved to localstorage
*
* @returns {string} - the mode that was set, either light or dark
* setup the theme mode - must be called once during app bootstrapping
* checks localStorage, then OS preference, defaults to light
* @returns {string} the mode that was set
*/
export function setupTheme() {
if (!browser) return modeLight;
let mode = getModeFromLocalStorage();
if (mode === modeNotSet) {
mode = getOSPreferredMode();
setModeToLocalStorage(mode);
}
changeMode(mode);
applyThemeToHTML(mode);
theme.set(mode);
return mode;
}
/**
* getOSPreferredMode is used to get the OS preferred light or dark mode
* @returns {string} - the mode that was set, either light or dark
* get the OS preferred light or dark mode
* @returns {string} either 'light' or 'dark'
*/
export const getOSPreferredMode = () => {
if (!browser) return modeLight;
const darkMediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
if (darkMediaQuery.matches) {
return modeDark;
}
return modeLight;
return darkMediaQuery.matches ? modeDark : modeLight;
};
/*
* This function is used to set the theme mode in the local storage
* @param {string} mode - light or dark
* @returns {string}
/**
* get the theme mode from localStorage
* @returns {string} the stored mode or 'not-set'
*/
export const getModeFromLocalStorage = () => {
if (!browser) return modeNotSet;
return localStorage.getItem(modeKey) ?? modeNotSet;
};
/**
* setModeToLocalStorage is used to set the theme mode in the local storage
* @param {string} mode - light or dark
* set the theme mode in localStorage
* @param {string} mode - either 'light' or 'dark'
*/
export const setModeToLocalStorage = (mode) => {
if (!browser) return;
localStorage.setItem(modeKey, mode);
};
/**
* changeMode is used to change the theme mode from light to dark or vice versa
* @param {string} mode - light or dark
* apply the theme to the HTML element by adding/removing the 'dark' class
* @param {string} mode - either 'light' or 'dark'
*/
const changeMode = (mode) => {
switch (mode) {
case modeLight:
theme.set(modeLight);
break;
case modeDark:
theme.set(modeDark);
break;
default:
throw new Error(`Unknown mode passed to changeMode: ${mode} - must be 'light' or 'dark'`);
const applyThemeToHTML = (mode) => {
if (!browser) return;
const html = document.documentElement;
if (mode === modeDark) {
html.classList.add('dark');
} else {
html.classList.remove('dark');
}
};
/**
* set up theme change listener for OS preference changes
*/
export const setupOSThemeListener = () => {
if (!browser) return;
const darkMediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
darkMediaQuery.addEventListener('change', (e) => {
// only update if user hasn't manually set a preference
const storedMode = getModeFromLocalStorage();
if (storedMode === modeNotSet) {
const newMode = e.matches ? modeDark : modeLight;
applyThemeToHTML(newMode);
theme.set(newMode);
}
});
};
+8 -1
View File
@@ -17,6 +17,7 @@
import DesktopMenu from '$lib/components/header/DesktopMenu.svelte';
import { hideIsLoading, showIsLoading } from '$lib/store/loading';
import Header from '$lib/components/header/Header.svelte';
import { setupTheme, setupOSThemeListener } from '$lib/theme.js';
// Removed feature flags import - no longer needed
// services
@@ -50,6 +51,10 @@
});
onMount(() => {
// initialize theme system
setupTheme();
setupOSThemeListener();
(async () => {
// handle session
// if the user already has a active session, then the session will be
@@ -197,7 +202,7 @@
};
</script>
<div class="flex flex-col">
<div class="flex flex-col min-w-[768px]">
<!-- global components -->
<DeveloperPanel />
<Loader />
@@ -235,6 +240,8 @@
/* font-family: "Phudu"; */
font-family: 'Titillium Web';
min-width: 768px;
overflow-x: auto;
}
:global(table) {
user-select: text;
+75 -35
View File
@@ -354,8 +354,10 @@
<FormColumns>
<FormColumn>
<!-- Basic Information Section -->
<div class="mb-6 pt-4 pb-2 border-b border-gray-200 w-full">
<h3 class="text-base font-medium text-pc-darkblue mb-3">Basic Information</h3>
<div class="mb-6 pt-4 pb-2 border-b border-gray-200 dark:border-gray-600 w-full">
<h3 class="text-base font-medium text-pc-darkblue dark:text-white mb-3">
Basic Information
</h3>
<TextField
required
minLength={1}
@@ -366,8 +368,10 @@
</div>
<!-- API Configuration Section -->
<div class="mb-6 pt-4 pb-2 border-b border-gray-200 w-full">
<h3 class="text-base font-medium text-pc-darkblue mb-3">API Configuration</h3>
<div class="mb-6 pt-4 pb-2 border-b border-gray-200 dark:border-gray-600 w-full">
<h3 class="text-base font-medium text-pc-darkblue dark:text-white mb-3">
API Configuration
</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="md:col-span-2">
<TextField
@@ -404,7 +408,9 @@
<!-- Headers and Body Section -->
<div class="mb-6 pt-4 pb-2 w-full">
<h3 class="text-base font-medium text-pc-darkblue mb-3">Request Details</h3>
<h3 class="text-base font-medium text-pc-darkblue dark:text-white mb-3">
Request Details
</h3>
<div class="space-y-5">
<TextareaField
optional={true}
@@ -420,9 +426,9 @@ X-Custom-Header: Hello Friend"
>
<div class="flex flex-col py-2 w-full">
<div class="flex items-center">
<p class="font-bold text-slate-600 py-2">Request Body</p>
<div class="bg-gray-100 ml-2 px-2 rounded-md">
<p class="text-slate-600 text-xs">optional</p>
<p class="font-bold text-slate-600 dark:text-gray-300 py-2">Request Body</p>
<div class="bg-gray-100 dark:bg-gray-700 ml-2 px-2 rounded-md">
<p class="text-slate-600 dark:text-gray-300 text-xs">optional</p>
</div>
</div>
<SimpleCodeEditor
@@ -441,8 +447,10 @@ X-Custom-Header: Hello Friend"
</div>
<!-- Response Validation Section -->
<div class="mb-6 pt-4 pb-2 border-b border-gray-200 w-full">
<h3 class="text-base font-medium text-pc-darkblue mb-3">Response Validation</h3>
<div class="mb-6 pt-4 pb-2 border-b border-gray-200 dark:border-gray-600 w-full">
<h3 class="text-base font-medium text-pc-darkblue dark:text-white mb-3">
Response Validation
</h3>
<div class="space-y-5">
<div>
<TextField
@@ -481,7 +489,9 @@ X-Custom-Header: Hello Friend"
<!-- Custom Fields Section -->
<div class="pt-4 pb-2 w-full">
<h3 class="text-base font-medium text-pc-darkblue mb-3">Custom Fields</h3>
<h3 class="text-base font-medium text-pc-darkblue dark:text-white mb-3">
Custom Fields
</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<TextField
@@ -536,9 +546,13 @@ X-Custom-Header: Hello Friend"
<div class="col-span-3 w-full overflow-y-auto px-6 py-4 space-y-6 select-text">
{#if !testResponse.error}
<!-- Successful Test -->
<div class="mb-6 pt-4 pb-2 border-b border-gray-200 w-full">
<h3 class="text-base font-medium text-pc-darkblue mb-3">Request Details</h3>
<div class="p-3 bg-gray-50 rounded-md border border-gray-200">
<div class="mb-6 pt-4 pb-2 border-b border-gray-200 dark:border-gray-600 w-full">
<h3 class="text-base font-medium text-pc-darkblue dark:text-white mb-3">
Request Details
</h3>
<div
class="p-3 bg-gray-50 dark:bg-gray-800 rounded-md border border-gray-200 dark:border-gray-600"
>
<div class="font-medium">
{testResponse.apiSender?.requestMethod}
{testResponse?.request?.url}
@@ -546,9 +560,13 @@ X-Custom-Header: Hello Friend"
</div>
</div>
<div class="mb-6 pt-4 pb-2 border-b border-gray-200 w-full">
<h3 class="text-base font-medium text-pc-darkblue mb-3">Request Headers</h3>
<div class="p-3 bg-gray-50 rounded-md border border-gray-200">
<div class="mb-6 pt-4 pb-2 border-b border-gray-200 dark:border-gray-600 w-full">
<h3 class="text-base font-medium text-pc-darkblue dark:text-white mb-3">
Request Headers
</h3>
<div
class="p-3 bg-gray-50 dark:bg-gray-800 rounded-md border border-gray-200 dark:border-gray-600"
>
<pre class="text-xs whitespace-pre-wrap overflow-x-auto max-h-60">{headersToString(
testResponse.request?.headers
) || 'No headers'}</pre>
@@ -556,17 +574,23 @@ X-Custom-Header: Hello Friend"
</div>
<div class="pt-4 pb-2 w-full">
<h3 class="text-base font-medium text-pc-darkblue mb-3">Request Body</h3>
<div class="p-3 bg-gray-50 rounded-md border border-gray-200">
<h3 class="text-base font-medium text-pc-darkblue dark:text-white mb-3">Request Body</h3>
<div
class="p-3 bg-gray-50 dark:bg-gray-800 rounded-md border border-gray-200 dark:border-gray-600"
>
<pre class="text-xs whitespace-pre-wrap overflow-x-auto max-h-80">{testResponse.request
?.body || 'Empty body'}</pre>
</div>
</div>
<!-- response -->
<div class="mb-6 pt-4 pb-2 border-b border-gray-200 w-full">
<h3 class="text-base font-medium text-pc-darkblue mb-3">Response Status</h3>
<div class="mb-6 pt-4 pb-2 border-b border-gray-200 dark:border-gray-600 w-full">
<h3 class="text-base font-medium text-pc-darkblue dark:text-white mb-3">
Response Status
</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="p-3 bg-gray-50 rounded-md border border-gray-200">
<div
class="p-3 bg-gray-50 dark:bg-gray-800 rounded-md border border-gray-200 dark:border-gray-600"
>
<div class="text-sm font-medium mb-1">Received:</div>
<div
class={testResponse.response?.code ==
@@ -577,23 +601,31 @@ X-Custom-Header: Hello Friend"
{testResponse.response?.code}
</div>
</div>
<div class="p-3 bg-gray-50 rounded-md border border-gray-200">
<div
class="p-3 bg-gray-50 dark:bg-gray-800 rounded-md border border-gray-200 dark:border-gray-600"
>
<div class="text-sm font-medium mb-1">Expected:</div>
<div>{testResponse.apiSender?.expectedResponseStatusCode}</div>
</div>
</div>
</div>
<div class="mb-6 pt-4 pb-2 border-b border-gray-200 w-full">
<h3 class="text-base font-medium text-pc-darkblue mb-3">Response Headers</h3>
<div class="mb-6 pt-4 pb-2 border-b border-gray-200 dark:border-gray-600 w-full">
<h3 class="text-base font-medium text-pc-darkblue dark:text-white mb-3">
Response Headers
</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="p-3 bg-gray-50 rounded-md border border-gray-200">
<div
class="p-3 bg-gray-50 dark:bg-gray-800 rounded-md border border-gray-200 dark:border-gray-600"
>
<div class="text-sm font-medium mb-1">Received:</div>
<pre class="text-xs whitespace-pre-wrap overflow-x-auto max-h-60">{headersToString(
testResponse.response?.headers
) || 'No headers'}</pre>
</div>
<div class="p-3 bg-gray-50 rounded-md border border-gray-200">
<div
class="p-3 bg-gray-50 dark:bg-gray-800 rounded-md border border-gray-200 dark:border-gray-600"
>
<div class="text-sm font-medium mb-1">Expected to contain:</div>
<pre class="text-xs whitespace-pre-wrap overflow-x-auto max-h-60">{testResponse
.apiSender?.expectedResponseHeaders || 'No validation specified'}</pre>
@@ -602,14 +634,18 @@ X-Custom-Header: Hello Friend"
</div>
<div class="pt-4 pb-2 w-full">
<h3 class="text-base font-medium text-pc-darkblue mb-3">Response Body</h3>
<h3 class="text-base font-medium text-pc-darkblue dark:text-white mb-3">Response Body</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="p-3 bg-gray-50 rounded-md border border-gray-200">
<div
class="p-3 bg-gray-50 dark:bg-gray-800 rounded-md border border-gray-200 dark:border-gray-600"
>
<div class="text-sm font-medium mb-1">Received:</div>
<pre class="text-xs whitespace-pre-wrap overflow-x-auto max-h-80">{testResponse
.response?.body || 'Empty response'}</pre>
</div>
<div class="p-3 bg-gray-50 rounded-md border border-gray-200">
<div
class="p-3 bg-gray-50 dark:bg-gray-800 rounded-md border border-gray-200 dark:border-gray-600"
>
<div class="text-sm font-medium mb-1">Expected to contain:</div>
<pre class="text-xs whitespace-pre-wrap overflow-x-auto max-h-80">{testResponse
.apiSender?.expectedResponseBody || 'No validation specified'}</pre>
@@ -618,9 +654,13 @@ X-Custom-Header: Hello Friend"
</div>
{:else}
<!-- Error Case -->
<div class="mb-6 pt-4 pb-2 border-b border-gray-200 w-full">
<h3 class="text-base font-medium text-pc-darkblue mb-3">Request Details</h3>
<div class="p-3 bg-gray-50 rounded-md border border-gray-200">
<div class="mb-6 pt-4 pb-2 border-b border-gray-200 dark:border-gray-600 w-full">
<h3 class="text-base font-medium text-pc-darkblue dark:text-white mb-3">
Request Details
</h3>
<div
class="p-3 bg-gray-50 dark:bg-gray-800 rounded-md border border-gray-200 dark:border-gray-600"
>
<div class="font-medium">
{testResponse.apiSender?.requestMethod}
{testResponse.apiSender?.requestURL}
@@ -636,11 +676,11 @@ X-Custom-Header: Hello Friend"
{/if}
<!-- Footer with Close Button -->
<div class="pt-4 border-t border-gray-200">
<div class="pt-4 border-t border-gray-200 dark:border-gray-600">
<div class="flex justify-end">
<button
type="button"
class="px-4 py-2 bg-white border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
class="px-4 py-2 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm text-sm font-medium text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
on:click={closeTestModal}
>
Close
@@ -661,7 +661,9 @@ Simulation URLs to allow:\n${allowListingData.simulationUrl}\n
<div class="col-span-3 w-full overflow-y-auto px-6 py-4 space-y-8">
<!-- Basic Information Section -->
<div class="w-full">
<h3 class="text-base font-medium text-pc-darkblue mb-3">Basic Information</h3>
<h3 class="text-base font-medium text-pc-darkblue dark:text-white mb-3">
Basic Information
</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<TextField
@@ -689,7 +691,9 @@ Simulation URLs to allow:\n${allowListingData.simulationUrl}\n
<!-- Delivery Configuration Section -->
<div class="w-full">
<h3 class="text-base font-medium text-pc-darkblue mb-3">Delivery Configuration</h3>
<h3 class="text-base font-medium text-pc-darkblue dark:text-white mb-3">
Delivery Configuration
</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
{#if formValues.templateType === 'Email' || !formValues.templateType}
@@ -721,7 +725,9 @@ Simulation URLs to allow:\n${allowListingData.simulationUrl}\n
<!-- Domain & URL Configuration Section -->
<div class="w-full">
<h3 class="text-base font-medium text-pc-darkblue mb-3">Domain & URL Configuration</h3>
<h3 class="text-base font-medium text-pc-darkblue dark:text-white mb-3">
Domain & URL Configuration
</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-x-6 gap-y-4">
<div>
<TextFieldSelect
@@ -764,7 +770,7 @@ Simulation URLs to allow:\n${allowListingData.simulationUrl}\n
<!-- Page Flow Section -->
<div class="w-full">
<h3 class="text-base font-medium text-pc-darkblue mb-3">Page Flow</h3>
<h3 class="text-base font-medium text-pc-darkblue dark:text-white mb-3">Page Flow</h3>
<div class="grid grid-cols-1 md:grid-cols-5 gap-6">
<div class="md:col-span-2 flex flex-col space-y-4">
<div>
+106 -46
View File
@@ -1042,7 +1042,9 @@
<Modal headerText={modalText} visible={isModalVisible} onClose={closeModal} {isSubmitting}>
<div class="relative flex justify-between items-center mb-8 w-full px-4">
<!-- Connector Line -->
<div class="absolute h-[2px] bg-gray-200 top-1/2 left-0 right-0 -translate-y-1/2 -z-10" />
<div
class="absolute h-[2px] bg-gray-200 dark:bg-gray-600 top-1/2 left-0 right-0 -translate-y-1/2 -z-10 transition-colors duration-200"
/>
{#each campaignSteps as step, index}
<div class="flex flex-col items-center w-20 sm:w-32">
@@ -1053,10 +1055,10 @@
transition-colors duration-200
${
currentStep > index + 1
? 'bg-blue-300 text-white'
? 'bg-blue-300 dark:bg-indigo-700 text-white'
: currentStep === index + 1
? 'bg-blue-600 text-white'
: 'bg-white text-gray-500 border-2 border-gray-300'
? 'bg-blue-600 dark:bg-indigo-600 text-white'
: 'bg-white dark:bg-gray-700 text-gray-500 dark:text-gray-300 border-2 border-gray-300 dark:border-gray-600'
}
`}
>
@@ -1083,7 +1085,9 @@
class={`
mt-2 text-xs sm:text-sm font-medium text-center
${
currentStep > index + 1 || currentStep === index + 1 ? 'text-blue-600' : 'text-gray-500'
currentStep > index + 1 || currentStep === index + 1
? 'text-blue-600 dark:text-blue-400'
: 'text-gray-500 dark:text-gray-400'
}
`}
>
@@ -1167,7 +1171,7 @@
Delivery start
</DateTimeField>
<button
class="text-cta-blue hover:text-blue-700 text-sm"
class="text-cta-blue hover:text-blue-700 dark:text-white dark:hover:text-gray-200 text-sm transition-colors duration-200"
on:click|preventDefault={() =>
(formValues.sendStartAt = new Date().toISOString())}
>
@@ -1178,7 +1182,9 @@
{#if formValues.sendStartAt}
<div class="pl-36 pt-4 pb-6">
<div class="flex flex-col gap-2">
<p class="text-sm font-semibold text-slate-600">
<p
class="text-sm font-semibold text-slate-600 dark:text-gray-300 transition-colors duration-200"
>
Distribution Speed
<span class="italic font-normal">
@@ -1237,7 +1243,7 @@
</DateTimeField>
{#if spreadOption === SPREAD_MANUAL}
<button
class="text-cta-blue hover:text-blue-700 text-sm"
class="text-cta-blue hover:text-blue-700 dark:text-white dark:hover:text-gray-200 text-sm transition-colors duration-200"
on:click|preventDefault={() => {
formValues.sendEndAt = new Date().toISOString();
}}
@@ -1472,38 +1478,56 @@
<!-- First Row: Basic Info and Recipients -->
<div class="grid grid-cols-2 gap-6">
<!-- Basic Information -->
<div class="bg-white p-6 rounded-lg shadow-sm">
<h3 class="text-xl font-semibold text-pc-darkblue mb-4 border-b pb-2">
<div
class="bg-white dark:bg-gray-800 p-6 rounded-lg shadow-sm transition-colors duration-200"
>
<h3
class="text-xl font-semibold text-pc-darkblue dark:text-white mb-4 border-b dark:border-gray-600 pb-2 transition-colors duration-200"
>
Basic Information
</h3>
<div class="grid grid-cols-[120px_1fr] gap-y-3">
<span class="text-grayblue-dark font-medium">Name:</span>
<span class="text-pc-darkblue">{formValues.name || 'Not set'}</span>
<span class="text-pc-darkblue dark:text-white"
>{formValues.name || 'Not set'}</span
>
<span class="text-grayblue-dark font-medium">Template:</span>
<span class="text-pc-darkblue">{formValues.template || 'Not set'}</span>
<span class="text-pc-darkblue dark:text-white"
>{formValues.template || 'Not set'}</span
>
<span class="text-grayblue-dark font-medium">Type:</span>
<span class="text-pc-darkblue">{formValues.isTest ? 'Test' : 'Production'}</span
<span class="text-pc-darkblue dark:text-white"
>{formValues.isTest ? 'Test' : 'Production'}</span
>
>
</div>
</div>
<!-- Recipients -->
<div class="bg-white p-6 rounded-lg shadow-sm">
<h3 class="text-xl font-semibold text-pc-darkblue mb-4 border-b pb-2">
<div
class="bg-white dark:bg-gray-800 p-6 rounded-lg shadow-sm transition-colors duration-200"
>
<h3
class="text-xl font-semibold text-pc-darkblue dark:text-white mb-4 border-b dark:border-gray-600 pb-2 transition-colors duration-200"
>
Recipients
</h3>
<div class="grid grid-cols-[120px_1fr] gap-y-3">
<span class="text-grayblue-dark font-medium">Groups:</span>
<span class="text-pc-darkblue">
<span
class="text-pc-darkblue dark:text-gray-100 transition-colors duration-200"
>
{formValues.recipientGroups.length
? formValues.recipientGroups.join(', ')
: 'None selected'}
</span>
<span class="text-grayblue-dark font-medium">Total:</span>
<span class="text-pc-darkblue">{formValues.selectedCount} recipients</span>
<span class="text-pc-darkblue dark:text-white"
>{formValues.selectedCount} recipients</span
>
</div>
</div>
</div>
@@ -1511,8 +1535,12 @@
<!-- Second Row: Email and Schedule -->
<div class="grid grid-cols-2 gap-6">
<!-- Email/API Sender Information -->
<div class="bg-white p-6 rounded-lg shadow-sm">
<h3 class="text-xl font-semibold text-pc-darkblue mb-4 border-b pb-2">
<div
class="bg-white dark:bg-gray-800 p-6 rounded-lg shadow-sm transition-colors duration-200"
>
<h3
class="text-xl font-semibold text-pc-darkblue dark:text-white mb-4 border-b dark:border-gray-600 pb-2 transition-colors duration-200"
>
Delivery Details
</h3>
{#await getTemplateDetails(formValues.template)}
@@ -1521,36 +1549,44 @@
{#if template?.apiSender}
<div class="grid grid-cols-[120px_1fr] gap-y-3">
<span class="text-grayblue-dark font-medium">Type:</span>
<span class="text-pc-darkblue font-semibold">API Sender</span>
<span class="text-pc-darkblue dark:text-white font-semibold"
>API Sender</span
>
<span class="text-grayblue-dark font-medium">Name:</span>
<span class="text-pc-darkblue">{template.apiSender.name || 'Not set'}</span>
<span class="text-pc-darkblue dark:text-white"
>{template.apiSender.name || 'Not set'}</span
>
{#if template?.email}
<span class="text-grayblue-dark font-medium">Email:</span>
<span class="text-pc-darkblue">{template.email.name || 'Not set'}</span>
<span class="text-pc-darkblue dark:text-white"
>{template.email.name || 'Not set'}</span
>
{/if}
</div>
{:else if template?.email}
<div class="grid grid-cols-[120px_1fr] gap-y-3">
<span class="text-grayblue-dark font-medium">Type:</span>
<span class="text-pc-darkblue font-semibold">SMTP</span>
<span class="text-pc-darkblue dark:text-white font-semibold">SMTP</span>
<span class="text-grayblue-dark font-medium">Name:</span>
<span class="text-pc-darkblue">{template.email.name || 'Not set'}</span>
<span class="text-pc-darkblue dark:text-white"
>{template.email.name || 'Not set'}</span
>
<span class="text-grayblue-dark font-medium">From:</span>
<span class="text-pc-darkblue"
<span class="text-pc-darkblue dark:text-white"
>{template.email.mailHeaderFrom || 'Not set'}</span
>
<span class="text-grayblue-dark font-medium">Mail from:</span>
<span class="text-pc-darkblue"
<span class="text-pc-darkblue dark:text-white"
>{template.email.mailEnvelopeFrom || 'Not set'}</span
>
<span class="text-grayblue-dark font-medium">Subject:</span>
<span class="text-pc-darkblue"
<span class="text-pc-darkblue dark:text-white"
>{template.email.mailHeaderSubject || 'Not set'}</span
>
</div>
@@ -1565,43 +1601,58 @@
</div>
<!-- Schedule -->
<div class="bg-white p-6 rounded-lg shadow-sm">
<h3 class="text-xl font-semibold text-pc-darkblue mb-4 border-b pb-2">
<div
class="bg-white dark:bg-gray-800 p-6 rounded-lg shadow-sm transition-colors duration-200"
>
<h3
class="text-xl font-semibold text-pc-darkblue dark:text-white mb-4 border-b dark:border-gray-600 pb-2 transition-colors duration-200"
>
Schedule
</h3>
<div class="grid grid-cols-[120px_1fr] gap-y-3">
<span class="text-grayblue-dark font-medium">Type:</span>
<span class="text-pc-darkblue capitalize">{scheduleType}</span>
<span class="text-pc-darkblue dark:text-white capitalize">{scheduleType}</span>
{#if scheduleType === 'basic'}
<span class="text-grayblue-dark font-medium">Start:</span>
<span class="text-pc-darkblue">
<span
class="text-pc-darkblue dark:text-gray-100 transition-colors duration-200"
>
<Datetime value={formValues.sendStartAt} />
<RelativeTime value={formValues.sendStartAt} />
</span>
<span class="text-grayblue-dark font-medium">End:</span>
<span class="text-pc-darkblue">
<span
class="text-grayblue-dark dark:text-gray-300 font-medium transition-colors duration-200"
>End:</span
>
<span
class="text-pc-darkblue dark:text-gray-100 transition-colors duration-200"
>
<Datetime value={formValues.sendEndAt} />
<RelativeTime value={formValues.sendEndAt} />
</span>
{#if spreadOption && spreadOption !== SPREAD_MANUAL}
<span class="text-grayblue-dark font-medium">Spread:</span>
<span class="text-pc-darkblue">
<span
class="text-pc-darkblue dark:text-gray-100 transition-colors duration-200"
>
{spreadOptionMap.byValue(spreadOption)}
</span>
{/if}
{:else if scheduleType === 'schedule'}
<span class="text-grayblue-dark font-medium">Active days:</span>
<span class="text-pc-darkblue">
<span
class="text-pc-darkblue dark:text-gray-100 transition-colors duration-200"
>
{formValues.constraintWeekDays.map((d) => dayMap[d]).join(', ') ||
'None selected'}
</span>
{#if formValues.contraintStartTime && formValues.contraintEndTime}
<span class="text-grayblue-dark font-medium">Hours:</span>
<span class="text-pc-darkblue">
<span class="text-pc-darkblue dark:text-white">
{formValues.contraintStartTime} - {formValues.contraintEndTime}
</span>
{/if}
@@ -1609,7 +1660,7 @@
{#if formValues.closeAt}
<span class="text-grayblue-dark font-medium">Close at:</span>
<span class="text-pc-darkblue">
<span class="text-pc-darkblue dark:text-white">
<Datetime value={formValues.closeAt} />
<RelativeTime value={formValues.closeAt} />
</span>
@@ -1620,13 +1671,17 @@
<!-- Third Row: Security & Privacy -->
<div class="grid grid-cols-1 gap-6">
<div class="bg-white p-6 rounded-lg shadow-sm">
<h3 class="text-xl font-semibold text-pc-darkblue mb-4 border-b pb-2">
<div
class="bg-white dark:bg-gray-800 p-6 rounded-lg shadow-sm transition-colors duration-200"
>
<h3
class="text-xl font-semibold text-pc-darkblue dark:text-white mb-4 border-b dark:border-gray-600 pb-2 transition-colors duration-200"
>
Security & Privacy
</h3>
<div class="grid grid-cols-[120px_1fr] gap-y-3">
<span class="text-grayblue-dark font-medium">IP Filtering:</span>
<span class="text-pc-darkblue">
<span class="text-pc-darkblue dark:text-white">
{#if allowDenyType === 'none'}
None
{:else}
@@ -1638,7 +1693,7 @@
</span>
<span class="text-grayblue-dark font-medium">Save data:</span>
<span class="text-pc-darkblue"
<span class="text-pc-darkblue dark:text-white"
>{formValues.saveSubmittedData ? 'Enabled' : 'Disabled'}</span
>
@@ -1651,17 +1706,20 @@
{#if formValues.webhookValue}
<span class="text-grayblue-dark font-medium">Webhook:</span>
<span class="text-pc-darkblue">{formValues.webhookValue}</span>
<span class="text-pc-darkblue dark:text-white">{formValues.webhookValue}</span
>
{/if}
{#if formValues.denyPageValue}
<span class="text-grayblue-dark font-medium">Deny Page:</span>
<span class="text-pc-darkblue">{formValues.denyPageValue}</span>
<span class="text-pc-darkblue dark:text-white"
>{formValues.denyPageValue}</span
>
{/if}
{#if formValues.anonymizeAt}
<span class="text-grayblue-dark font-medium">Anonymize at:</span>
<span class="text-pc-darkblue">
<span class="text-pc-darkblue dark:text-white">
<Datetime value={formValues.anonymizeAt} />
<RelativeTime value={formValues.anonymizeAt} />
</span>
@@ -1674,11 +1732,13 @@
</FormColumns>
{/if}
<FormError message={modalError} />
<div class="col-span-3 flex justify-between items-center w-full mt-2 border-t py-6">
<div
class="col-span-3 flex justify-between items-center w-full mt-2 border-t dark:border-gray-600 py-6 transition-colors duration-200"
>
{#if currentStep > 1}
<button
type="button"
class="inline-flex items-center px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
class="inline-flex items-center px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm text-sm font-medium text-gray-700 dark:text-gray-200 bg-white dark:bg-gray-700 hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
on:click={previousStep}
>
<svg
+123 -83
View File
@@ -38,6 +38,7 @@
import EventName from '$lib/components/table/EventName.svelte';
import { goto } from '$app/navigation';
import { globalButtonDisabledAttributes } from '$lib/utils/form';
import FileField from '$lib/components/FileField.svelte';
// services
const appStateService = AppStateService.instance;
@@ -1062,18 +1063,20 @@
<!-- First Row: Details and Timeline -->
<div class="grid grid-cols-1 lg:grid-cols-2">
<!-- Primary Campaign Info -->
<div class="bg-white py-6 rounded-lg">
<h3 class="text-xl font-semibold text-pc-darkblue mb-4 border-b pb-2">Details</h3>
<div class="py-6 rounded-lg">
<h3 class="text-xl font-semibold text-pc-darkblue dark:text-white mb-4 border-b pb-2">
Details
</h3>
<div class="grid grid-cols-[120px_1fr] gap-y-3">
<span class="text-grayblue-dark font-medium">Status:</span>
<span class="text-pc-darkblue font-semibold">
<span class="text-pc-darkblue dark:text-white font-semibold">
{toEvent(campaign.notableEventName).name}
</span>
<span class="text-grayblue-dark font-medium">Template:</span>
<span class="text-pc-darkblue">
<span class="text-pc-darkblue dark:text-white">
<button
class="cursor-pointer text-cta-blue underline hover:opacity-75"
class="cursor-pointer text-cta-blue dark:text-white underline hover:opacity-75"
on:click={() => {
openTemplateModal(templateMap.byValue(campaign.template));
}}
@@ -1083,10 +1086,10 @@
</span>
<span class="text-grayblue-dark font-medium">Groups:</span>
<span class="text-pc-darkblue">
<span class="text-pc-darkblue dark:text-white">
{#each campaign.recipientGroups as group, i}
<a
class="hover:underline text-cta-blue hover:opacity-75 underline"
class="hover:underline text-cta-blue dark:text-white hover:opacity-75 underline"
href="/recipient/group/{recipientGroupMap.byValue(group)}"
target="_blank">{group}</a
>{#if i !== (campaign.recipientGroups?.length ?? 0) - 1}
@@ -1096,18 +1099,20 @@
</span>
<span class="text-grayblue-dark font-medium">Type:</span>
<span class="text-pc-darkblue">{isSelfManaged ? 'Self Managed' : 'Scheduled'}</span>
<span class="text-pc-darkblue dark:text-white"
>{isSelfManaged ? 'Self Managed' : 'Scheduled'}</span
>
<span class="text-grayblue-dark font-medium">
{campaign?.allowDeny ? (campaign.allowDeny.allowed ? 'Allow' : 'Deny') : ''}
IP Filters:
</span>
<span class="text-pc-darkblue">
<span class="text-pc-darkblue dark:text-white">
{#if campaign.allowDeny?.length}
{#each campaign.allowDeny as allowDeny, i}
<a
href="/ip-filter/?edit={allowDeny.id}"
class="text-pc-blue hover:underline"
class="text-cta-blue dark:text-white hover:underline"
target="_blank"
>
{allowDeny.name}
@@ -1121,7 +1126,7 @@
</span>
<span class="text-grayblue-dark font-medium">Webhook:</span>
<span class="text-pc-darkblue">
<span class="text-pc-darkblue dark:text-white">
{#if campaign.webhookID}
Yes
{:else}
@@ -1130,44 +1135,60 @@
</span>
<span class="text-grayblue-dark font-medium">Data Saving:</span>
<span class="text-pc-darkblue">
<span class="text-pc-darkblue dark:text-white">
{campaign.saveSubmittedData ? 'Enabled' : 'Disabled'}
</span>
<span class="text-grayblue-dark font-medium">Test:</span>
<span class="text-pc-darkblue">{campaign.isTest ? 'Yes' : 'No'}</span>
<span class="text-pc-darkblue dark:text-white">{campaign.isTest ? 'Yes' : 'No'}</span>
</div>
</div>
<div class="bg-white p-6 rounded-lg">
<h3 class="text-xl font-semibold text-pc-darkblue mb-4 border-b pb-2">Timeline</h3>
<div class="p-6 rounded-lg">
<h3 class="text-xl font-semibold text-pc-darkblue dark:text-white mb-4 border-b pb-2">
Timeline
</h3>
<div class="grid grid-cols-[120px_1fr] gap-y-3">
<span class="text-grayblue-dark font-medium">Created:</span>
<span class="text-pc-darkblue"><Datetime value={campaign.createdAt} /></span>
<span class="text-pc-darkblue dark:text-white"
><Datetime value={campaign.createdAt} /></span
>
{#if !isSelfManaged}
<span class="text-grayblue-dark font-medium">Delivery start:</span>
<span class="text-pc-darkblue"><Datetime value={campaign.sendStartAt} /></span>
<span class="text-pc-darkblue dark:text-white"
><Datetime value={campaign.sendStartAt} /></span
>
<span class="text-grayblue-dark font-medium">Delivery finish:</span>
<span class="text-pc-darkblue"><Datetime value={campaign.sendEndAt} /></span>
<span class="text-pc-darkblue dark:text-white"
><Datetime value={campaign.sendEndAt} /></span
>
{/if}
<span class="text-grayblue-dark font-medium">Close At:</span>
<span class="text-pc-darkblue"><Datetime value={campaign.closeAt} /></span>
<span class="text-pc-darkblue dark:text-white"
><Datetime value={campaign.closeAt} /></span
>
<span class="text-grayblue-dark font-medium">Closed:</span>
<span class="text-pc-darkblue"><Datetime value={campaign.closedAt} /></span>
<span class="text-pc-darkblue dark:text-white"
><Datetime value={campaign.closedAt} /></span
>
<span class="text-grayblue-dark font-medium">Anonymize At:</span>
<span class="text-pc-darkblue"><Datetime value={campaign.anonymizeAt} /></span>
<span class="text-pc-darkblue dark:text-white"
><Datetime value={campaign.anonymizeAt} /></span
>
<span class="text-grayblue-dark font-medium">Anonymized:</span>
<span class="text-pc-darkblue"><Datetime value={campaign.anonymizedAt} /></span>
<span class="text-pc-darkblue dark:text-white"
><Datetime value={campaign.anonymizedAt} /></span
>
</div>
{#if campaign.constraintWeekDays}
<div class="bg-white py-6 rounded-lg">
<div class="py-6 rounded-lg">
<div class="flex gap-8">
<!-- Days Schedule -->
<div class="flex-1">
@@ -1244,8 +1265,10 @@
<!-- Second Row: Schedule Constraints and Actions -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div></div>
<div class="bg-white p-6 rounded-lg">
<h3 class="text-xl font-semibold text-pc-darkblue mb-4 border-b pb-2">Actions</h3>
<div class="p-6 rounded-lg">
<h3 class="text-xl font-semibold text-pc-darkblue dark:text-white mb-4 border-b pb-2">
Actions
</h3>
<div class="space-y-4">
<!-- Action Buttons -->
<div class="flex flex-wrap gap-3">
@@ -1292,20 +1315,10 @@
<!-- Upload Section -->
<div class="border-t pt-4">
<div>
<label
for="reported-csv-upload"
class="block text-sm font-medium text-gray-700 mb-2"
>
<FileField accept=".csv" on:change={onUploadReportedCSV}>
Upload Reported CSV
</label>
<input
type="file"
id="reported-csv-upload"
accept=".csv"
on:change={onUploadReportedCSV}
class="block w-full text-sm text-gray-900 file:mr-4 file:py-2 file:px-4 file:rounded-md file:border-0 file:text-sm file:font-semibold file:bg-reported file:text-white hover:file:opacity-80"
/>
<p class="mt-1 text-xs text-gray-500">
</FileField>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
CSV format: "Reported by" (email), "Date reported(UTC+02:00)"
</p>
</div>
@@ -1551,15 +1564,17 @@
>
<div class="space-y-6">
<!-- Full-width Landing Page Flow -->
<div class="bg-white p-6 rounded-lg">
<h3 class="text-xl font-semibold text-pc-darkblue mb-4 border-b pb-2">Phishing Flow</h3>
<div class="p-6 rounded-lg">
<h3 class="text-xl font-semibold text-pc-darkblue dark:text-white mb-4 border-b pb-2">
Phishing Flow
</h3>
<!-- Enhanced Flow Visualization -->
<div class="flex items-center justify-center mb-6 text-sm">
<div class="flex items-center flex-wrap justify-center gap-2">
<!-- First block is always the delivery method -->
<div class="text-center px-3 py-2 bg-pc-lightblue rounded">
<div class="font-medium">
<div class="text-center px-3 py-2 bg-pc-lightblue dark:bg-blue-600 rounded">
<div class="font-medium text-gray-800 dark:text-white">
{#if template.email}
Email
{:else}
@@ -1575,8 +1590,8 @@
<!-- Before Landing -->
{#if template.beforeLandingPage}
<div class="text-center px-3 py-2 bg-pc-lightblue rounded">
<div class="font-medium">Before Landing</div>
<div class="text-center px-3 py-2 bg-pc-lightblue dark:bg-blue-600 rounded">
<div class="font-medium text-gray-800 dark:text-white">Before Landing</div>
</div>
<!-- Only show arrow if there's a next step -->
{#if template.landingPage}
@@ -1586,8 +1601,8 @@
<!-- Main Landing -->
{#if template.landingPage}
<div class="text-center px-3 py-2 bg-pc-lightblue rounded">
<div class="font-medium">Main Landing</div>
<div class="text-center px-3 py-2 bg-pc-lightblue dark:bg-blue-600 rounded">
<div class="font-medium text-gray-800 dark:text-white">Main Landing</div>
</div>
<!-- Only show arrow if there's a next step -->
{#if template.afterLandingPage || template.afterLandingPageRedirectURL}
@@ -1597,14 +1612,14 @@
<!-- After Landing or Redirect -->
{#if template.afterLandingPage}
<div class="text-center px-3 py-2 bg-pc-lightblue rounded">
<div class="font-medium">After Landing</div>
<div class="text-center px-3 py-2 bg-pc-lightblue dark:bg-blue-600 rounded">
<div class="font-medium text-gray-800 dark:text-white">After Landing</div>
</div>
{/if}
{#if template.afterLandingPageRedirectURL}
<div class="mx-2"></div>
<div class="text-center px-3 py-2 bg-pc-lightorange rounded">
<div class="font-medium">Redirect</div>
<div class="text-center px-3 py-2 bg-pc-lightorange dark:bg-orange-600 rounded">
<div class="font-medium text-gray-800 dark:text-white">Redirect</div>
</div>
{/if}
</div>
@@ -1614,22 +1629,26 @@
<!-- Basic Info and Email Config -->
<div class="grid grid-cols-2 gap-6">
<!-- Basic Info Section -->
<div class="bg-white p-6 rounded-lg">
<h3 class="text-xl font-semibold text-pc-darkblue mb-4 border-b pb-2">
<div class="p-6 rounded-lg">
<h3 class="text-xl font-semibold text-pc-darkblue dark:text-white mb-4 border-b pb-2">
Basic Information
</h3>
<div class="grid grid-cols-[120px_1fr] gap-y-3">
<span class="text-grayblue-dark font-medium">Name:</span>
<span class="text-pc-darkblue">{template.name ?? ''}</span>
<span class="text-pc-darkblue dark:text-white">{template.name ?? ''}</span>
<span class="text-grayblue-dark font-medium">Query Key:</span>
<span class="text-pc-darkblue">{template.urlIdentifier?.name ?? ''}</span>
<span class="text-pc-darkblue dark:text-white"
>{template.urlIdentifier?.name ?? ''}</span
>
<span class="text-grayblue-dark font-medium">State Key:</span>
<span class="text-pc-darkblue">{template.stateIdentifier?.name ?? ''}</span>
<span class="text-pc-darkblue dark:text-white"
>{template.stateIdentifier?.name ?? ''}</span
>
<span class="text-grayblue-dark font-medium">Delivery :</span>
<span class="text-pc-darkblue">
<span class="text-pc-darkblue dark:text-white">
{#if template.email}
Email ({template.email.name ?? ''})
{:else}
@@ -1638,38 +1657,49 @@
</span>
<span class="text-grayblue-dark font-medium">Before Page:</span>
<span class="text-pc-darkblue">{template.beforeLandingPage?.name ?? ''}</span>
<span class="text-pc-darkblue dark:text-white"
>{template.beforeLandingPage?.name ?? ''}</span
>
<span class="text-grayblue-dark font-medium">Main Page:</span>
<span class="text-pc-darkblue">{template.landingPage?.name ?? ''}</span>
<span class="text-pc-darkblue dark:text-white">{template.landingPage?.name ?? ''}</span>
<span class="text-grayblue-dark font-medium">After Page:</span>
<span class="text-pc-darkblue">{template.afterLandingPage?.name ?? ''}</span>
<span class="text-pc-darkblue dark:text-white"
>{template.afterLandingPage?.name ?? ''}</span
>
<span class="text-grayblue-dark font-medium">Redirect URL:</span>
<span class="text-pc-darkblue">{template.afterLandingPageRedirectURL ?? ''}</span>
<span class="text-pc-darkblue dark:text-white"
>{template.afterLandingPageRedirectURL ?? ''}</span
>
</div>
</div>
<!-- Email Configuration -->
{#if template.email}
<div class="bg-white p-6 rounded-lg">
<h3 class="text-xl font-semibold text-pc-darkblue mb-4 border-b pb-2">Email</h3>
<div class="p-6 rounded-lg">
<h3 class="text-xl font-semibold text-pc-darkblue dark:text-white mb-4 border-b pb-2">
Email
</h3>
<div class="grid grid-cols-[120px_1fr] gap-y-3">
<span class="text-grayblue-dark font-medium">Name:</span>
<span class="text-pc-darkblue">{template.email.name}</span>
<span class="text-pc-darkblue dark:text-white">{template.email.name}</span>
<span class="text-grayblue-dark font-medium">Envelope:</span>
<span class="text-pc-darkblue">{template.email.mailEnvelopeFrom}</span>
<span class="text-pc-darkblue dark:text-white">{template.email.mailEnvelopeFrom}</span
>
<span class="text-grayblue-dark font-medium">From:</span>
<span class="text-pc-darkblue">{template.email.mailHeaderFrom}</span>
<span class="text-pc-darkblue dark:text-white">{template.email.mailHeaderFrom}</span>
<span class="text-grayblue-dark font-medium">Subject:</span>
<span class="text-pc-darkblue">{template.email.mailHeaderSubject}</span>
<span class="text-pc-darkblue dark:text-white"
>{template.email.mailHeaderSubject}</span
>
<span class="text-grayblue-dark font-medium">Tracking:</span>
<span class="text-pc-darkblue"
<span class="text-pc-darkblue dark:text-white"
>{template.email.addTrackingPixel ? 'Enabled' : 'Disabled'}</span
>
</div>
@@ -1680,28 +1710,32 @@
<!-- Domain and SMTP Config -->
<div class="grid grid-cols-2 gap-6">
<!-- Domain Configuration -->
<div class="bg-white p-6 rounded-lg">
<h3 class="text-xl font-semibold text-pc-darkblue mb-4 border-b pb-2">Domain</h3>
<div class="p-6 rounded-lg">
<h3 class="text-xl font-semibold text-pc-darkblue dark:text-white mb-4 border-b pb-2">
Domain
</h3>
<div class="grid grid-cols-[120px_1fr] gap-y-3">
<span class="text-grayblue-dark font-medium">Host Site:</span>
<span class="text-pc-darkblue">{template.domain.hostWebsite ? 'Yes' : 'No'}</span>
<span class="text-pc-darkblue dark:text-white"
>{template.domain.hostWebsite ? 'Yes' : 'No'}</span
>
<span class="text-grayblue-dark font-medium">Domain:</span>
<span class="text-pc-darkblue">
<span class="text-pc-darkblue dark:text-white">
<a
href="https://{template.domain?.name}"
target="_blank"
class="text-pc-blue hover:underline"
class="text-cta-blue dark:text-white hover:underline"
>
{template.domain?.name}
</a>
</span>
<span class="text-grayblue-dark font-medium">URL Path:</span>
<span class="text-pc-darkblue">{template.urlPath}</span>
<span class="text-pc-darkblue dark:text-white">{template.urlPath}</span>
<span class="text-grayblue-dark font-medium">TLS:</span>
<span class="text-pc-darkblue">
<span class="text-pc-darkblue dark:text-white">
{template.domain.managedTLS ? 'Managed' : template.domain.ownManagedTLS ? 'Own' : ''}
</span>
</div>
@@ -1709,33 +1743,39 @@
<!-- SMTP/API Configuration -->
{#if template.smtpConfiguration || template.apiSender}
<div class="bg-white p-6 rounded-lg">
<h3 class="text-xl font-semibold text-pc-darkblue mb-4 border-b pb-2">
<div class="p-6 rounded-lg">
<h3 class="text-xl font-semibold text-pc-darkblue dark:text-white mb-4 border-b pb-2">
{template.smtpConfiguration ? 'Email SMTP' : 'API Sender'}
</h3>
<div class="grid grid-cols-[120px_1fr] gap-y-3">
{#if template.smtpConfiguration}
<span class="text-grayblue-dark font-medium">Name:</span>
<span class="text-pc-darkblue">{template.smtpConfiguration.name}</span>
<span class="text-pc-darkblue dark:text-white"
>{template.smtpConfiguration.name}</span
>
<span class="text-grayblue-dark font-medium">Host:</span>
<span class="text-pc-darkblue">{template.smtpConfiguration.host}</span>
<span class="text-pc-darkblue dark:text-white"
>{template.smtpConfiguration.host}</span
>
<span class="text-grayblue-dark font-medium">Port:</span>
<span class="text-pc-darkblue">{template.smtpConfiguration.port}</span>
<span class="text-pc-darkblue dark:text-white"
>{template.smtpConfiguration.port}</span
>
<span class="text-grayblue-dark font-medium">Username:</span>
<span class="text-pc-darkblue">
<span class="text-pc-darkblue dark:text-white">
{template.smtpConfiguration.username || 'Not configured'}
</span>
<span class="text-grayblue-dark font-medium">Allow insecure: </span>
<span class="text-pc-darkblue">
<span class="text-pc-darkblue dark:text-white">
{!template.smtpConfiguration.ignoreCertErrors ? 'Disabled' : 'Enabled'}
</span>
{:else}
<span class="text-grayblue-dark font-medium">API Sender:</span>
<span class="text-pc-darkblue">{template.apiSender?.name}</span>
<span class="text-pc-darkblue dark:text-white">{template.apiSender?.name}</span>
{/if}
</div>
</div>
@@ -1778,7 +1818,7 @@
? `Are you sure you want to send the campaign ${getMessageType()} again to:`
: `Are you sure you want to send the campaign ${getMessageType()} to:`}
</p>
<div class="bg-gray-50 p-3 rounded mb-4">
<div class="bg-gray-50 dark:bg-gray-700 p-3 rounded mb-4">
<p class="font-medium">{sendMessageRecipient.name}</p>
<p class="text-gray-600">{sendMessageRecipient.email}</p>
<p class="text-xs text-blue-600 mt-1">
+9 -7
View File
@@ -72,6 +72,8 @@
// Toggle for including test campaigns
let includeTestCampaigns = false;
// Use consistent colors with campaign detail page - these are already well-suited for both light and dark modes
// Handler for when toggle changes
const handleToggleChange = async () => {
// Wait for binding to update
@@ -292,7 +294,7 @@
<main>
<div class="flex justify-between">
<Headline>Dashboard</Headline>
<div class="flex gap-4 items-baseline">
<div class="flex gap-4 items-center">
<CheckboxField
bind:value={includeTestCampaigns}
on:change={handleToggleChange}
@@ -449,13 +451,13 @@
<StatsCard
title="Completed Campaigns"
value={finished}
borderColor="border-campaign-completed"
iconColor="text-campaign-completed"
borderColor="border-message-read"
iconColor="text-message-read"
>
<svg
slot="icon"
xmlns="http://www.w3.org/2000/svg"
class="h-5 w-5 ml-2 text-campaign-completed"
class="h-5 w-5 ml-2 text-message-read"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
@@ -474,13 +476,13 @@
<StatsCard
title="Repeat Offenders"
value={repeatOffenders}
borderColor="border-repeart-submissions"
iconColor="text-repeart-submissions"
borderColor="border-submitted-data"
iconColor="text-submitted-data"
>
<svg
slot="icon"
xmlns="http://www.w3.org/2000/svg"
class="h-5 w-5 ml-2 text-repeart-submissions"
class="h-5 w-5 ml-2 text-submitted-data"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
+9 -5
View File
@@ -592,8 +592,10 @@
<FormColumns>
<FormColumn>
<!-- Domain Information Section -->
<div class="mb-6 pt-4 pb-2 border-b border-gray-200 w-full">
<h3 class="text-base font-medium text-pc-darkblue mb-3">Domain Information</h3>
<div class="mb-6 pt-4 pb-2 border-b border-gray-200 dark:border-gray-600 w-full">
<h3 class="text-base font-medium text-pc-darkblue dark:text-white mb-3">
Domain Information
</h3>
<div class="space-y-6">
<TextField
minLength={3}
@@ -628,9 +630,11 @@
</div>
</div>
<!-- TLS Configuration Section -->
<div class="mb-6 pt-4 pb-2 w-full">
<h3 class="text-base font-medium text-pc-darkblue mb-3">TLS Configuration</h3>
<!-- SSL Configuration Section -->
<div class="pt-4 pb-2 w-full">
<h3 class="text-base font-medium text-pc-darkblue dark:text-white mb-3">
SSL Configuration
</h3>
<div class="space-y-6">
<SelectSquare
label="Managed TLS"
+10 -4
View File
@@ -139,16 +139,22 @@
<HeadTitle title="Setup" />
<div class="inset-0 z-50 min-h-screen">
<div
class="inset-0 z-50 min-h-screen bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100 transition-colors duration-200"
>
<div class="flex flex-col justify-center py-12 sm:px-6 lg:px-8">
<div class="flex justify-center">
<img src="/logo-blue.svg" class="w-48" alt="Phishing Club" />
</div>
<p class="mt-2 text-center text-sm text-gray-600">
<p
class="mt-2 text-center text-sm text-gray-600 dark:text-gray-300 transition-colors duration-200"
>
Complete the setup to get started with Phishing Club
</p>
<div class="sm:mx-auto sm:max-w-2xl mt-8">
<div
class="sm:mx-auto sm:max-w-2xl mt-8 bg-white dark:bg-gray-800 rounded-lg shadow-lg dark:shadow-gray-900/50 transition-colors duration-200"
>
<div class="flex justify-between items-center mb-8 w-full px-4">
{#each steps as step, index}
<div class="flex flex-col items-center w-32">
@@ -263,7 +269,7 @@
{#if currentStep > 1}
<button
type="button"
class="inline-flex items-center px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
class="inline-flex items-center px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm text-sm font-medium text-gray-700 dark:text-gray-200 bg-white dark:bg-gray-700 hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
on:click={previousStep}
>
<svg
+39 -14
View File
@@ -208,7 +208,7 @@
<HeadTitle title="Sign in" />
<main
class="h-screen grid-cols-1 grid md:grid-cols-1 lg:grid-cols-2 xl:grid-cols-2 2xl:grid-cols-2"
class="h-screen grid-cols-1 grid md:grid-cols-1 lg:grid-cols-2 xl:grid-cols-2 2xl:grid-cols-2 bg-white dark:bg-gray-900 transition-colors duration-200"
>
<div class="flex items-center justify-center h-full">
<img
@@ -221,7 +221,7 @@
>
<div class="flex flex-col items-center justify-center w-full p-4">
<h1
class="text-4xl md:text-5xl lg:text-5xl xl:text-5xl 2xl:text-5xl font-titilium font-bold uppercase text-pc-darkblue text-center"
class="text-4xl md:text-5xl lg:text-5xl xl:text-5xl 2xl:text-5xl font-titilium font-bold uppercase text-pc-darkblue dark:text-white text-center transition-colors duration-200"
>
Please sign in
</h1>
@@ -230,7 +230,7 @@
<div class="flex flex-col items-center justify-center w-full p-px md:p-px lg:p-4">
{#if loginError}
<div
class="flex justify-center w-9/10 bg-message-red border-pc-red border text-center py-4 font-titilium"
class="flex justify-center w-9/10 bg-message-red dark:bg-red-900 border-pc-red dark:border-red-700 border text-center py-4 font-titilium text-gray-900 dark:text-white transition-colors duration-200"
>
{loginError}
</div>
@@ -248,7 +248,9 @@
submitOnEnter
/>
<div class="flex flex-col w-full p-4 h-24">
<label for="Password" class="text-md font-semibold font-titilium text-pc-darkblue"
<label
for="Password"
class="text-md font-semibold font-titilium text-pc-darkblue dark:text-white transition-colors duration-200"
>Password</label
>
<div class="relative flex items-center justify-end">
@@ -259,9 +261,17 @@
on:click={handleClick}
>
{#if isPasswordVisible}
<img src="/view.svg" alt="view" />
<img
src="/view.svg"
alt="view"
class="dark:filter dark:brightness-0 dark:invert transition-all duration-200"
/>
{:else}
<img src="/toggle-view.svg" alt="toggle view" />
<img
src="/toggle-view.svg"
alt="toggle view"
class="dark:filter dark:brightness-0 dark:invert transition-all duration-200"
/>
{/if}
</button>
<input
@@ -281,14 +291,18 @@
type={inputType}
id="Password"
name="Password"
class="relative w-full p-2 rounded bg-pc-lightblue focus:outline-none focus:ring-0 focus:border-cta-blue focus:border-2"
class="relative w-full p-2 rounded bg-pc-lightblue dark:bg-gray-700 focus:outline-none focus:ring-0 focus:border-cta-blue dark:focus:border-blue-400 focus:border-2 text-gray-900 dark:text-white transition-colors duration-200"
/>
</div>
</div>
<CTAbutton disabled={isSubmitting} />
{#if isSSOEnabled}
<div class="absolute bottom-12">
<div class="text-center font-bold">SSO</div>
<div
class="text-center font-bold text-gray-900 dark:text-white transition-colors duration-200"
>
SSO
</div>
<a href="/api/v1/sso/entra-id/login">
<img src="/ms-login-light.svg" alt="Login with Microsoft" />
</a>
@@ -303,15 +317,22 @@
>
<FormColumns>
<FormColumn>
<p class="text-center text-lg">Enter the MFA code from your authenticator app</p>
<p
class="text-center text-lg text-gray-900 dark:text-white transition-colors duration-200"
>
Enter the MFA code from your authenticator app
</p>
<Input fieldName={'MFA Code'} type="text" bind:value={formValues.mfaTOTP} />
<div class="flex flex-col">
<p class="text-sm">
<p
class="text-sm text-gray-700 dark:text-gray-300 transition-colors duration-200"
>
Forgot your MFA code? Recover your code <button
type="button"
on:click={showMFARecoveryModal}
class="text-cta-blue underline">here</button
class="text-cta-blue dark:text-blue-400 underline hover:text-blue-700 dark:hover:text-blue-300 transition-colors duration-200"
>here</button
>
</p>
</div>
@@ -319,7 +340,7 @@
</FormColumns>
<FormError message={mfaError} />
<div
class="row-start-7 py-4 row-span-2 col-start-1 col-span-3 border-t-2 w-full flex flex-row justify-center items-center sm:justify-center md:justify-center lg:justify-end xl:justify-end 2xl:justify-end"
class="row-start-7 py-4 row-span-2 col-start-1 col-span-3 border-t-2 dark:border-gray-600 w-full flex flex-row justify-center items-center sm:justify-center md:justify-center lg:justify-end xl:justify-end 2xl:justify-end transition-colors duration-200"
>
<FormButton>Verify</FormButton>
</div>
@@ -338,7 +359,11 @@
<FormColumns>
<FormColumn>
<div class="px-16 py-8">
<p class="text-center text-lg">Please enter the MFA recovery code</p>
<p
class="text-center text-lg text-gray-900 dark:text-white transition-colors duration-200"
>
Please enter the MFA recovery code
</p>
<Input
fieldName={'MFA Recovery Code'}
type="text"
@@ -350,7 +375,7 @@
<FormError message={mfaRecoveryLoginError} />
<div
class="row-start-7 py-4 row-span-2 col-start-1 col-span-3 border-t-2 w-full flex flex-row justify-center items-center sm:justify-center md:justify-center lg:justify-end xl:justify-end 2xl:justify-end"
class="row-start-7 py-4 row-span-2 col-start-1 col-span-3 border-t-2 dark:border-gray-600 w-full flex flex-row justify-center items-center sm:justify-center md:justify-center lg:justify-end xl:justify-end 2xl:justify-end transition-colors duration-200"
>
<FormButton>Verify</FormButton>
</div>
+7 -5
View File
@@ -6,27 +6,29 @@
import { globalButtonDisabledAttributes } from '$lib/utils/form.js';
import Headline from '$lib/components/Headline.svelte';
import TextField from '$lib/components/TextField.svelte';
import TableCell from '$lib/components/table/TableCell.svelte';
import TableRow from '$lib/components/table/TableRow.svelte';
import TableCell from '$lib/components/table/TableCell.svelte';
import TableUpdateButton from '$lib/components/table/TableUpdateButton.svelte';
import TableDeleteButton from '$lib/components/table/TableDeleteButton2.svelte';
import { addToast } from '$lib/store/toast';
import FormError from '$lib/components/FormError.svelte';
import { addToast } from '$lib/store/toast';
import { AppStateService } from '$lib/service/appState';
import TableCellAction from '$lib/components/table/TableCellAction.svelte';
import TableCellEmpty from '$lib/components/table/TableCellEmpty.svelte';
import FormGrid from '$lib/components/FormGrid.svelte';
import TableCellAction from '$lib/components/table/TableCellAction.svelte';
import Modal from '$lib/components/Modal.svelte';
import FormGrid from '$lib/components/FormGrid.svelte';
import BigButton from '$lib/components/BigButton.svelte';
import FormColumns from '$lib/components/FormColumns.svelte';
import FormColumn from '$lib/components/FormColumn.svelte';
import FormFooter from '$lib/components/FormFooter.svelte';
import Table from '$lib/components/table/Table.svelte';
import HeadTitle from '$lib/components/HeadTitle.svelte';
import { getModalText } from '$lib/utils/common';
import TableCopyButton from '$lib/components/table/TableCopyButton.svelte';
import { showIsLoading, hideIsLoading } from '$lib/store/loading.js';
import Editor from '$lib/components/editor/Editor.svelte';
import TableDropDownEllipsis from '$lib/components/table/TableDropDownEllipsis.svelte';
import DeleteAlert from '$lib/components/modal/DeleteAlert.svelte';
import Editor from '$lib/components/editor/Editor.svelte';
import { fetchAllRows } from '$lib/utils/api-utils';
import { BiMap } from '$lib/utils/maps';
import AutoRefresh from '$lib/components/AutoRefresh.svelte';
+43 -17
View File
@@ -361,9 +361,13 @@
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8">
<!-- Profile Settings -->
<div
class="bg-white p-6 rounded-lg shadow-sm border border-gray-100 min-h-[300px] flex flex-col"
class="bg-white dark:bg-gray-800 p-6 rounded-lg shadow-sm dark:shadow-gray-900/50 border border-gray-100 dark:border-gray-700 min-h-[300px] flex flex-col transition-colors duration-200"
>
<h2 class="text-xl font-semibold text-gray-700 mb-6">Account Details</h2>
<h2
class="text-xl font-semibold text-gray-700 dark:text-gray-200 mb-6 transition-colors duration-200"
>
Account Details
</h2>
<Form
fullHeight
on:submit={async () => {
@@ -402,9 +406,13 @@
<!-- Password Settings -->
<div
class="bg-white p-6 rounded-lg shadow-sm border border-gray-100 min-h-[300px] flex flex-col"
class="bg-white dark:bg-gray-800 p-6 rounded-lg shadow-sm dark:shadow-gray-900/50 border border-gray-100 dark:border-gray-700 min-h-[300px] flex flex-col transition-colors duration-200"
>
<h2 class="text-xl font-semibold text-gray-700 mb-6">Password Settings</h2>
<h2
class="text-xl font-semibold text-gray-700 dark:text-gray-200 mb-6 transition-colors duration-200"
>
Password Settings
</h2>
{#if !isSSOUser}
<Form on:submit={onClickChangePassword}>
<div class="flex flex-col h-full">
@@ -437,16 +445,22 @@
</div>
</Form>
{:else}
<div class="bg-gray-50 p-4 rounded-md text-gray-600">
<div
class="bg-gray-50 dark:bg-gray-800 p-4 rounded-md text-gray-600 dark:text-gray-300"
>
Password changes are disabled for SSO users.
</div>
{/if}
</div>
<!-- MFA Section -->
<div
class="bg-white p-6 rounded-lg shadow-sm border border-gray-100 min-h-[300px] flex flex-col"
class="bg-white dark:bg-gray-800 p-6 rounded-lg shadow-sm dark:shadow-gray-900/50 border border-gray-100 dark:border-gray-700 min-h-[300px] flex flex-col transition-colors duration-200"
>
<h2 class="text-xl font-semibold text-gray-700 mb-6">Multi-Factor Authentication</h2>
<h2
class="text-xl font-semibold text-gray-700 dark:text-gray-200 mb-6 transition-colors duration-200"
>
Multi-Factor Authentication
</h2>
{#if !isSSOUser}
{#if isMFAEnabled}
<div class="flex flex-col h-full pt-5 w-60">
@@ -484,7 +498,9 @@
</Form>
{/if}
{:else}
<div class="bg-gray-50 p-4 rounded-md text-gray-600">
<div
class="bg-gray-50 dark:bg-gray-800 p-4 rounded-md text-gray-600 dark:text-gray-300"
>
MFA is managed through your SSO provider.
</div>
{/if}
@@ -492,9 +508,13 @@
<!-- API Key Section -->
<div
class="bg-white p-6 rounded-lg shadow-sm border border-gray-100 min-h-[300px] flex flex-col"
class="bg-white dark:bg-gray-800 p-6 rounded-lg shadow-sm dark:shadow-gray-900/50 border border-gray-100 dark:border-gray-700 min-h-[300px] flex flex-col transition-colors duration-200"
>
<h2 class="text-xl font-semibold text-gray-700 mb-6">API Access</h2>
<h2
class="text-xl font-semibold text-gray-700 dark:text-gray-200 mb-6 transition-colors duration-200"
>
API Access
</h2>
<Form fullHeight on:submit={onClickCreateAPIKey}>
<div class="flex flex-col h-full">
{#if !!apiKey.length}
@@ -508,7 +528,9 @@
<Button size={'large'} on:click={openShowRemoveAPIKeyModal}>Delete Key</Button>
</div>
{:else}
<div class="bg-gray-50 p-4 rounded-md text-gray-600 mb-4">
<div
class="bg-gray-50 dark:bg-gray-800 p-4 rounded-md text-gray-600 dark:text-gray-300 mb-4"
>
No API key currently exists.
</div>
<div class="mt-auto pt-4">
@@ -578,9 +600,11 @@
</div>
<!-- Alternative Method -->
<div class="border-gray-200 pt-4">
<h3 class="text-lg font-medium text-gray-700 mb-2">Can't scan the QR code?</h3>
<p class="text-gray-600 mb-2">
<div class="border-gray-200 dark:border-gray-600 pt-4">
<h3 class="text-lg font-medium text-gray-700 dark:text-gray-200 mb-2">
Can't scan the QR code?
</h3>
<p class="text-gray-600 dark:text-gray-300 mb-2">
Enter this code manually in your authenticator app:
</p>
<button
@@ -594,9 +618,11 @@
</div>
<!-- Step 2 -->
<div class="border-t border-gray-200 pt-4">
<h3 class="text-xl font-semibold text-gray-700 mb-2">2. Verify Setup</h3>
<p class="text-gray-600 mb-4">
<div class="border-t border-gray-200 dark:border-gray-600 pt-4">
<h3 class="text-xl font-semibold text-gray-700 dark:text-gray-200 mb-2">
2. Verify Setup
</h3>
<p class="text-gray-600 dark:text-gray-300 mb-4">
Enter the 6-digit code from your authenticator app:
</p>
<TextField
+92 -30
View File
@@ -442,9 +442,13 @@
<div class="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-4 gap-8">
<!-- SSO Card -->
<div
class="bg-white p-6 rounded-lg shadow-sm border border-gray-100 min-h-[300px] flex flex-col"
class="bg-white dark:bg-gray-800 p-6 rounded-lg shadow-sm dark:shadow-gray-900/50 border border-gray-100 dark:border-gray-700 min-h-[300px] flex flex-col transition-colors duration-200"
>
<h2 class="text-xl font-semibold text-gray-700 mb-6">Single Sign-On</h2>
<h2
class="text-xl font-semibold text-gray-700 dark:text-gray-200 mb-6 transition-colors duration-200"
>
Single Sign-On
</h2>
<div class="flex flex-col h-full pt-4">
<div class="bg-gray-50 rounded-md p-3">
{#if isSSOEnabled}
@@ -476,9 +480,13 @@
<!-- General Settings Card -->
<div
class="bg-white p-6 rounded-lg shadow-sm border border-gray-100 min-h-[300px] flex flex-col"
class="bg-white dark:bg-gray-800 p-6 rounded-lg shadow-sm dark:shadow-gray-900/50 border border-gray-100 dark:border-gray-700 min-h-[300px] flex flex-col transition-colors duration-200"
>
<h2 class="text-xl font-semibold text-gray-700 mb-6">General Settings</h2>
<h2
class="text-xl font-semibold text-gray-700 dark:text-gray-200 mb-6 transition-colors duration-200"
>
General Settings
</h2>
<Form on:submit={onClickUpdateSettings} fullHeight>
<div class="flex flex-col h-full">
<div>
@@ -504,9 +512,13 @@
<!-- Logging Card -->
<div
class="bg-white p-6 rounded-lg shadow-sm border border-gray-100 min-h-[300px] flex flex-col"
class="bg-white dark:bg-gray-800 p-6 rounded-lg shadow-sm dark:shadow-gray-900/50 border border-gray-100 dark:border-gray-700 min-h-[300px] flex flex-col transition-colors duration-200"
>
<h2 class="text-xl font-semibold text-gray-700 mb-6">Logging</h2>
<h2
class="text-xl font-semibold text-gray-700 dark:text-gray-200 mb-6 transition-colors duration-200"
>
Logging
</h2>
<Form fullHeight>
<div class="flex flex-col h-full">
<div>
@@ -532,9 +544,13 @@
<!-- Import Section -->
<div
class="bg-white p-6 rounded-lg shadow-sm border border-gray-100 min-h-[300px] flex flex-col"
class="bg-white dark:bg-gray-800 p-6 rounded-lg shadow-sm dark:shadow-gray-900/50 border border-gray-100 dark:border-gray-700 min-h-[300px] flex flex-col transition-colors duration-200"
>
<h2 class="text-xl font-semibold text-gray-700 mb-6">Import</h2>
<h2
class="text-xl font-semibold text-gray-700 dark:text-gray-200 mb-6 transition-colors duration-200"
>
Import
</h2>
<div class="flex flex-col h-full">
<div class="space-y-4">
<FileField name="importFile" accept=".zip" on:change={(e) => onSetImportFile(e)}>
@@ -549,12 +565,16 @@
Import pages and emails as company templates
</label>
{#if importForCompany}
<div class="bg-blue-50 p-3 rounded-md text-sm text-blue-700">
<div
class="bg-blue-50 dark:bg-blue-900/30 p-3 rounded-md text-sm text-blue-700 dark:text-blue-200 transition-colors duration-200"
>
<strong>Company Import:</strong><br /> Pages and emails will be imported for this company.
Assets will be imported as global/shared resources.
</div>
{:else}
<div class="bg-gray-50 p-3 rounded-md text-sm text-gray-600">
<div
class="bg-gray-50 dark:bg-gray-700 p-3 rounded-md text-sm text-gray-600 dark:text-gray-300 transition-colors duration-200"
>
<strong>Global Import:</strong> All templates and assets will be imported as shared
resources.
</div>
@@ -579,31 +599,45 @@
<!-- Backup Section -->
<div
class="bg-white p-6 rounded-lg shadow-sm border border-gray-100 min-h-[300px] flex flex-col"
class="bg-white dark:bg-gray-800 p-6 rounded-lg shadow-sm dark:shadow-gray-900/50 border border-gray-100 dark:border-gray-700 min-h-[300px] flex flex-col transition-colors duration-200"
>
<h2 class="text-xl font-semibold text-gray-700 mb-6">Backup</h2>
<h2
class="text-xl font-semibold text-gray-700 dark:text-gray-200 mb-6 transition-colors duration-200"
>
Backup
</h2>
<div class="flex flex-col h-full">
<div class="space-y-4">
<p class="text-gray-600 text-sm">
<p class="text-gray-600 dark:text-gray-300 text-sm transition-colors duration-200">
Create a backup of database, assets, attachments and certificates.
</p>
{#if availableBackups.length > 0}
<div class="bg-gray-50 p-3 rounded-md">
<h4 class="font-medium text-gray-900 mb-2">Available:</h4>
<div
class="bg-gray-50 dark:bg-gray-700 p-3 rounded-md transition-colors duration-200"
>
<h4
class="font-medium text-gray-900 dark:text-gray-100 mb-2 transition-colors duration-200"
>
Available:
</h4>
<div class="space-y-3">
{#each availableBackups as backup}
<div class="flex items-start justify-between gap-4 text-sm">
<div class="flex flex-col min-w-0 flex-1">
<span class="text-gray-700 text-xs font-medium">
<span
class="text-gray-700 dark:text-gray-200 text-xs font-medium transition-colors duration-200"
>
{new Date(backup.createdAt).toLocaleString()}
</span>
<span class="text-gray-400 text-xs">
<span
class="text-gray-400 dark:text-gray-400 text-xs transition-colors duration-200"
>
{(backup.size / 1024 / 1024).toFixed(1)} MB
</span>
</div>
<button
class="px-3 py-1 bg-blue-600 hover:bg-blue-700 text-white text-xs rounded transition-colors flex-shrink-0"
class="px-3 py-1 bg-blue-600 hover:bg-blue-700 dark:bg-blue-700 dark:hover:bg-blue-800 text-white text-xs rounded transition-colors flex-shrink-0"
on:click={() => downloadBackup(backup.name)}
>
Download
@@ -613,7 +647,9 @@
</div>
</div>
{:else if !isLoadingBackups}
<div class="bg-gray-50 p-3 rounded-md text-sm text-gray-600">
<div
class="bg-gray-50 dark:bg-gray-700 p-3 rounded-md text-sm text-gray-600 dark:text-gray-300 transition-colors duration-200"
>
No backups available yet.
</div>
{/if}
@@ -631,7 +667,7 @@
{#if isImportResultModalVisible && importResult}
<Modal headerText="Import Summary" bind:visible={isImportResultModalVisible}>
<div
class="p-6 max-h-[80vh] overflow-y-auto"
class="p-6 max-h-[80vh] overflow-y-auto bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 transition-colors duration-200"
on:scroll={() => {}}
bind:this={importModalContent}
>
@@ -640,20 +676,30 @@
<div class="grid grid-cols-3 gap-6">
<!-- Assets -->
<div>
<h3 class="font-semibold text-gray-900 mb-2">Assets (Global/Shared)</h3>
<h3
class="font-semibold text-gray-900 dark:text-gray-100 mb-2 transition-colors duration-200"
>
Assets (Global/Shared)
</h3>
<ul class="space-y-1">
<li>Created: {importResult.assets_created}</li>
<li>Skipped: {importResult.assets_skipped}</li>
<li>Errors: {importResult.assets_errors}</li>
</ul>
<p class="text-xs text-gray-500 mt-1">
<p
class="text-xs text-gray-500 dark:text-gray-400 mt-1 transition-colors duration-200"
>
Assets are always imported as global resources
</p>
</div>
<!-- Pages -->
<div>
<h3 class="font-semibold text-gray-900 mb-2">Pages</h3>
<h3
class="font-semibold text-gray-900 dark:text-gray-100 mb-2 transition-colors duration-200"
>
Pages
</h3>
<ul class="space-y-1">
<li>Created: {importResult.pages_created}</li>
<li>Updated: {importResult.pages_updated}</li>
@@ -664,7 +710,11 @@
<!-- Emails -->
<div>
<h3 class="font-semibold text-gray-900 mb-2">Emails</h3>
<h3
class="font-semibold text-gray-900 dark:text-gray-100 mb-2 transition-colors duration-200"
>
Emails
</h3>
<ul class="space-y-1">
<li>Created: {importResult.emails_created}</li>
<li>Updated: {importResult.emails_updated}</li>
@@ -829,26 +879,34 @@
{/if}
<!-- Version and Update Info -->
<div class="mt-8 text-sm text-gray-600 border-t border-gray-100 pt-4">
<div
class="mt-8 text-sm text-gray-600 dark:text-gray-300 border-t border-gray-100 dark:border-gray-700 pt-4 transition-colors duration-200"
>
<div class="flex items-center gap-4 flex-wrap">
<button
on:click|preventDefault={() => onClickCopy('.version-text')}
class="flex items-center hover:bg-gray-100 py-2 px-4 rounded-md text-gray-700 transition-colors"
class="flex items-center hover:bg-gray-100 dark:hover:bg-gray-700 py-2 px-4 rounded-md text-gray-700 dark:text-gray-300 transition-colors duration-200"
>
<span class="version-text font-mono">Version: {version}</span>
<img class="ml-2 w-4 h-4" src="/icon-copy.svg" alt="copy version" />
</button>
<span>|</span>
{#if updateAvailable}
<a href="/settings/update/" class="text-blue-600 hover:underline">Update Available</a>
<a
href="/settings/update/"
class="text-blue-600 dark:text-white hover:underline transition-colors duration-200"
>Update Available</a
>
{:else}
<span class="text-gray-500">Up to date</span>
<span class="text-gray-500 dark:text-gray-400 transition-colors duration-200"
>Up to date</span
>
{/if}
<span>|</span>
<button
on:click={checkForUpdate}
disabled={isCheckingUpdate}
class="text-blue-600 hover:underline disabled:opacity-50 disabled:cursor-not-allowed"
class="text-blue-600 dark:text-white hover:underline disabled:opacity-50 disabled:cursor-not-allowed transition-colors duration-200"
>
{#if isCheckingUpdate}
Checking...
@@ -857,7 +915,11 @@
{/if}
</button>
<span>|</span>
<a href="/licenses.txt" class="text-blue-600 hover:underline">View Licenses</a>
<a
href="/licenses.txt"
class="text-blue-600 dark:text-white hover:underline transition-colors duration-200"
>View Licenses</a
>
</div>
</div>
{/if}
@@ -481,8 +481,10 @@
<FormColumns>
<FormColumn>
<!-- Basic Configuration Section -->
<div class="mb-6 pt-4 pb-2 border-b border-gray-200 w-full">
<h3 class="text-base font-medium text-pc-darkblue mb-3">Basic Configuration</h3>
<div class="mb-6 pt-4 pb-2 border-b border-gray-200 dark:border-gray-600 w-full">
<h3 class="text-base font-medium text-pc-darkblue dark:text-white mb-3">
Basic Configuration
</h3>
<div class="space-y-6">
<TextField
required
@@ -510,8 +512,10 @@
</div>
<!-- Authentication Section -->
<div class="mb-6 pt-4 pb-2 border-b border-gray-200 w-full">
<h3 class="text-base font-medium text-pc-darkblue mb-3">Authentication</h3>
<div class="mb-6 pt-4 pb-2 border-b border-gray-200 dark:border-gray-600 w-full">
<h3 class="text-base font-medium text-pc-darkblue dark:text-white mb-3">
Authentication
</h3>
<div class="space-y-6">
<TextField
minLength={1}
@@ -531,7 +535,9 @@
<!-- Security Settings -->
<div class="pt-4 pb-2 w-full">
<h3 class="text-base font-medium text-pc-darkblue mb-3">Security Settings</h3>
<h3 class="text-base font-medium text-pc-darkblue dark:text-white mb-3">
Security Settings
</h3>
<div>
<SelectSquare
label="TLS Certificate Validation"
+4
View File
@@ -95,6 +95,10 @@ export default {
'70vw': '70vw',
'80vw': '80vw',
'90vw': '90vw'
},
minWidth: {
app: '768px',
desktop: '1024px'
}
}
}