Add CSS via SCSS to the frontend build pipeline

This commit is contained in:
Eli Ribble 2026-03-21 19:14:51 +00:00
parent 1e67c0090d
commit f3c818a48f
No known key found for this signature in database
12 changed files with 924 additions and 94 deletions

12
ts/global.d.ts vendored Normal file
View file

@ -0,0 +1,12 @@
import * as bootstrap from 'bootstrap';
declare global {
interface Window {
Alpine: any;
SSEManager: any;
createAppPlanning: any;
bootstrap: typeof bootstrap;
}
}
export {};

View file

@ -1,5 +1,55 @@
import Alpine from './vendor/alpinejs-3.15.8.js';
import { createApp } from 'vue';
import App from './app.vue';
import { SSEManager } from './sse-manager';
import { SetupSidebar } from "./sidebar";
import 'maplibre-gl/dist/maplibre-gl.css';
// Import Bootstrap SCSS
import './style/style.scss';
// Import Bootstrap JavaScript and make it available globally
import * as bootstrap from 'bootstrap';
window.bootstrap = bootstrap;
import { Planning } from './app/planning';
// Make Alpine available on window for inline Alpine
window.Alpine = Alpine;
// Make SSEManager available to all the JavaScript
window.SSEManager = SSEManager;
function createAppPlanning() {
const app = createApp({
data() {
return {
count: 0
}
}
});
}
window.createAppPlanning = createAppPlanning;
// Wait for DOM to be ready, then initialize Alpine
document.addEventListener("DOMContentLoaded", () => {
Alpine.start();
SSEManager.connect("/api/events");
SetupSidebar();
});
interface GreetingComponent {
message: string;
name: string;
updateMessage(): void;
}
Alpine.data('greeting', (): GreetingComponent => ({
message: 'Welcome to Alpine + TypeScript!',
name: 'World',
updateMessage() {
this.message = 'Message updated at ' + new Date().toLocaleTimeString();
}
}));
createApp(App).mount('#app');

75
ts/sidebar.ts Normal file
View file

@ -0,0 +1,75 @@
export function SetupSidebar() {
console.log("setting up sidebar");
var popoverTriggerList = [].slice.call(
document.querySelectorAll('[data-bs-toggle="popover"]'),
);
var popoverList = popoverTriggerList.map(function (popoverTriggerEl) {
return new bootstrap.Popover(popoverTriggerEl);
});
console.log("Initialized ", popoverTriggerList.length, " popovers");
var tooltipTriggerList = [].slice.call(
document.querySelectorAll('[data-bs-toggle="tooltip"]'),
);
var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
let t = new bootstrap.Tooltip(tooltipTriggerEl);
return t;
});
console.log("Initialized ", tooltipTriggerList.length, " tooltips");
restoreLocalStorage();
setTooltipsForSidebar();
SSEManager.subscribe("*", function (e) {
if (e.type != "heartbeat") {
updateUserState();
}
});
document.addEventListener("alpine:init", () => {
Alpine.store("user", USER);
})
document.getElementById("sidebarToggle").addEventListener("click", () => {
const sidebar = document.getElementById("sidebar");
sidebar.classList.toggle("collapsed");
document.getElementById("content").classList.toggle("expanded");
setTooltipsForSidebar();
localStorage.setItem(
"sidebar.expanded",
(!sidebar.classList.contains("collapsed")).toString(),
);
});
}
function restoreLocalStorage() {
const expanded = localStorage.getItem("sidebar.expanded");
if (expanded == "false") {
document.getElementById("sidebar").classList.add("collapsed");
document.getElementById("content").classList.add("expanded");
} else {
document.getElementById("sidebar").classList.remove("collapsed");
document.getElementById("content").classList.remove("expanded");
localStorage.setItem("sidebar.expanded", "true");
}
}
function setTooltipsForSidebar() {
const sidebarTooltips = document.querySelectorAll(
'#sidebar [data-bs-toggle="tooltip"]',
);
const isExpanded = document
.getElementById("content")
.classList.contains("expanded");
sidebarTooltips.forEach((t) => {
const tooltip = bootstrap.Tooltip.getOrCreateInstance(t);
if (isExpanded) {
tooltip.enable();
} else {
tooltip.disable();
}
});
}
async function updateUserState() {
const response = await fetch("/api/user/self");
const data = await response.json();
// Update properties instead of replacing the whole store which leverages Alpine's reactivity
const store_user = Alpine.store("user");
Object.keys(data).forEach(key => {
store_user[key] = data[key];
});
}

140
ts/style/sidebar.scss Normal file
View file

@ -0,0 +1,140 @@
.logo-container {
display: flex;
justify-content: center;
align-items: center;
transition: all 0.3s ease;
}
.logo {
max-width: 100%;
height: auto;
transition: all 0.3s ease;
}
#sidebar {
background-color: $off-white;
min-height: 100vh;
transition: all 0.3s;
width: 250px;
position: fixed;
z-index: 1000;
padding: 20px;
}
#sidebar.collapsed {
width: 70px;
padding: 20px 10px;
}
/* Logo style when sidebar is collapsed */
#sidebar.collapsed .logo-container {
width: 100%;
}
#sidebar.collapsed .logo-img {
max-width: 40px; /* smaller size for collapsed state */
}
#content {
transition: all 0.3s;
margin-left: 250px;
padding: 10px;
width: calc(100% - 250px);
}
#content.expanded {
margin-left: 70px;
width: calc(100% - 70px);
}
.sidebar-header {
padding-bottom: 20px;
border-bottom: 1px solid $off-black;
margin-bottom: 20px;
overflow: hidden;
white-space: nowrap;
display: flex;
justify-content: center; /* Center for the logo */
}
.sidebar-menu {
list-style: none;
padding: 0;
}
.sidebar-menu li {
padding: 10px 0;
}
.sidebar-menu li a {
text-decoration: none;
color: $off-black;
display: flex;
align-items: center;
overflow: hidden;
white-space: nowrap;
}
.sidebar-menu li a:hover {
color: $primary;
}
.sidebar-menu .menu-icon {
font-size: 1.2rem;
min-width: 30px;
display: flex;
justify-content: center;
}
.sidebar-menu .menu-icon svg {
width: 1.5em;
height: 1.5em;
}
.sidebar-menu .menu-text {
transition: opacity 0.3s;
}
#sidebar.collapsed .menu-text {
opacity: 0;
visibility: hidden;
width: 0;
}
#sidebar.collapsed .sidebar-header h4 {
opacity: 0;
visibility: hidden;
}
#sidebar.collapsed .sidebar-menu .menu-icon {
min-width: 100%;
font-size: 1.5rem;
}
#sidebarToggle {
position: absolute;
left: calc(250px - 15px);
top: 50%;
transform: translateY(-50%);
z-index: 1050;
width: 30px;
height: 30px;
border-radius: 50%;
border: 1px solid #dee2e6;
display: flex;
align-items: center;
transition: left 0.3s;
padding: 0;
}
#sidebarToggle i {
transition: transform 0.3s;
}
#sidebar.collapsed > #sidebarToggle {
left: calc(70px - 15px);
}
#sidebar > #sidebarToggle i {
position: relative;
left: 5px;
}
#sidebar.collapsed > #sidebarToggle i {
transform: rotate(180deg);
}

63
ts/style/style.scss Normal file
View file

@ -0,0 +1,63 @@
@use "sass:map";
// 1. Include specific theme variables
$primary: #F76436;
$secondary: #3C552D;
$success: #8BAE67;
$warning: #FFC01B;
$danger: #6b2737;
$info: #D7B26D;
$dark: #3b1002;
$light: #fde1d8;
$off-white: #F8F9FA;
$off-black: #495057;
$primary-light-4: #FAA489;
// 2. Configure color contrast
$color-contrast-dark: #000;
$color-contrast-light: #fff;
$min-contrast-ratio: 2.0;
$custom-colors: (
"color1": $primary,
"color2": $secondary,
"color3": $success,
"color4": $danger,
"color5": $warning,
"color6": $info,
);
$theme-colors: map.merge(
(
"primary": $primary,
"secondary": $secondary,
"success": $success,
"danger": $danger,
"warning": $warning,
"info": $info,
"dark": $dark,
"light": $light
),
$custom-colors
);
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
// Make custom SVG icons about the same size as other icons
i.bi svg {
height: 18px;
width: 18px;
}
@import "bootstrap/scss/functions";
// Import Bootstrap's variables (this merges with your custom variables)
@import "bootstrap/scss/variables";
// Import Bootstrap's mixins
@import "bootstrap/scss/mixins";
// Import all of Bootstrap (or pick specific components)
@import "bootstrap/scss/bootstrap";
@import "./sidebar.scss";