Files
bambuddy/frontend/src/index.css
T
maziggy eb5154f61a feat(queue): tabbed page, batch grouping, multi-drag, Gantt timeline
Restructures the queue page around three tabs (Queue / History / Timeline)
  and adds first-class batch grouping plus a real time-based timeline.

  Queue tab
  - Layout toggle: Sort by Position (flat list) or Group by Printer (per-
    printer section cards with aggregate count / time / weight headers).
  - Batch grouping: pending items sharing a batch_id render as a single
    collapsible row with aggregate stats; children draggable within the
    batch only. Per-batch collapse state in localStorage.
  - Multi-drag: dragging any selected row moves all selected items as a
    contiguous block via DragOverlay (+N ghost).
  - Selection bar gains a Group as batch action when 2+ ungrouped items
    are selected. Ungroup lives on the batch parent row.

  History tab
  - Two-line rich rows: filament color swatch + weight + type, user
    attribution, inline error message on failed / skipped rows.
  - Responsive 1 / 2 / 3 column grid so a long history uses available
    width instead of stretching one row per line.
  - Batch siblings group into a collapsible parent with status-rollup
    chips (3 OK / 1 failed / etc).
  - Thumbnail hover preview shows the full image at 192x192 next to the
    small thumb.

  Timeline tab
  - Replaces the hourly-list view with a Gantt swimlane: one row per
    printer (plus per target_model and unassigned), horizontal hour
    axis, jobs as bars positioned by start time and sized by duration.
  - Live NOW marker.
  - Only committed schedules are rendered: currently printing items,
    pending items with scheduled_time, and pending ASAP behind an active
    print. Staged (manual_start), waiting (waiting_reason), and ASAP
    jobs on idle printers are filtered out.
  - 24h rolling window with 12h step controls.
  - Per-bar tooltip with start, end, progress, batch name.

  Backend
  - POST /queue/batches creates a batch, optionally assigning existing
    pending item_ids (manual grouping) or returning an empty batch the
    client can attach to subsequent /queue/ POSTs.
  - POST /queue/batches/{id}/ungroup clears batch_id from all members
    (skipping items the caller does not own) and deletes the batch row
    when no members remain.
  - POST /queue/ accepts an optional batch_id and validates that the
    batch exists, is active, and the caller may modify it. The existing
    quantity > 1 auto-batch path still fires when no batch_id is sent.

  PrintModal
  - When N plates from one source are queued in a single submission
    (model assignment or single printer), the modal pre-creates a batch
    and passes its id to each addToQueue call so multi-plate jobs land
    grouped automatically. Falls back to ungrouped items if the batch
    pre-create fails.
2026-06-16 10:45:30 +02:00

533 lines
12 KiB
CSS

@import "tailwindcss";
/* Inter, self-hosted (#1460). Bambuddy is a local-first, offline-capable PWA;
pulling the font from fonts.googleapis.com at runtime broke the install on
Android (CSP-blocked cross-origin fetch) and meant the UI couldn't render
offline. These are the variable woff2 files Google Fonts serves — one file
covers every weight via the variable axis, so font-weight is a 100-900 range.
Files live in frontend/public/fonts/ and are served same-origin. */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 100 900;
font-display: swap;
src: url('/fonts/inter-latin.woff2') format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 100 900;
font-display: swap;
src: url('/fonts/inter-latin-ext.woff2') format('woff2');
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* Enable class-based dark mode for Tailwind v4 */
@custom-variant dark (&:where(.dark, .dark *));
@theme {
/* Accent colors - use CSS variables for theming */
--color-bambu-green: var(--accent);
--color-bambu-green-light: var(--accent-light);
--color-bambu-green-dark: var(--accent-dark);
/* Semantic status colors - fixed, don't change with accent */
--color-status-ok: var(--status-ok);
--color-status-error: var(--status-error);
--color-status-warning: var(--status-warning);
/* Theme-aware colors via CSS variables */
--color-bambu-dark: var(--bg-primary);
--color-bambu-dark-secondary: var(--bg-secondary);
--color-bambu-dark-tertiary: var(--bg-tertiary);
--color-bambu-gray: var(--text-muted);
--color-bambu-gray-light: var(--text-secondary);
--color-bambu-gray-dark: var(--text-tertiary);
}
/* ============================================
BASE DEFAULTS
============================================ */
html {
font-size: 14.4px;
}
:root {
/* Default accent color (green) */
--accent: #00ae42;
--accent-light: #00c64d;
--accent-dark: #009438;
/* Semantic status colors - these never change with accent theme */
--status-ok: #22c55e; /* green-500 - always green for success/online/ok */
--status-error: #ef4444; /* red-500 - always red for error/offline/failed */
--status-warning: #f59e0b; /* amber-500 - always amber for warnings */
/* Default light mode background (neutral) */
--bg-primary: #f5f5f5;
--bg-secondary: #ffffff;
--bg-tertiary: #e5e5e5;
--text-primary: #1a1a1a;
--text-secondary: #4a4a4a;
--text-muted: #6b6b6b;
--text-tertiary: #808080;
--border-color: #d4d4d4;
/* Default style (classic) */
--card-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
--glow-color: transparent;
font-family: 'Inter', system-ui, sans-serif;
line-height: 1.5;
font-weight: 400;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
/* Dark mode base */
.dark {
--bg-primary: #1a1a1a;
--bg-secondary: #2d2d2d;
--bg-tertiary: #3d3d3d;
--text-primary: #ffffff;
--text-secondary: #a0a0a0;
--text-muted: #808080;
--text-tertiary: #4a4a4a;
--border-color: #3d3d3d;
--card-shadow: 0 4px 16px rgba(0, 0, 0, 0.4);
}
/* ============================================
LAYER 1: BACKGROUND PALETTES
============================================ */
/* Light mode backgrounds */
.bg-neutral {
/* Default - already set in :root */
}
.bg-warm {
--bg-primary: #faf8f5;
--bg-secondary: #fffefa;
--bg-tertiary: #e8e4dd;
--text-primary: #2d2a26;
--text-secondary: #5c5750;
--text-muted: #7a756c;
--text-tertiary: #9a9590;
--border-color: #d8d4cc;
}
.bg-cool {
--bg-primary: #f0f4f8;
--bg-secondary: #ffffff;
--bg-tertiary: #dce4ec;
--text-primary: #1a2530;
--text-secondary: #4a5568;
--text-muted: #6b7a8a;
--text-tertiary: #8a9aaa;
--border-color: #c8d4e0;
}
/* Dark mode backgrounds */
.dark.bg-neutral {
--bg-primary: #1a1a1a;
--bg-secondary: #2d2d2d;
--bg-tertiary: #3d3d3d;
--text-primary: #ffffff;
--text-secondary: #a0a0a0;
--text-muted: #808080;
--text-tertiary: #4a4a4a;
--border-color: #3d3d3d;
}
.dark.bg-warm {
--bg-primary: #1c1a18;
--bg-secondary: #2e2a26;
--bg-tertiary: #3e3a36;
--text-primary: #f5f0ea;
--text-secondary: #b0a898;
--text-muted: #8a8278;
--text-tertiary: #5a5248;
--border-color: #3e3a36;
}
.dark.bg-cool {
--bg-primary: #181c20;
--bg-secondary: #262c32;
--bg-tertiary: #363e46;
--text-primary: #f0f4f8;
--text-secondary: #98a8b8;
--text-muted: #788898;
--text-tertiary: #4a5a6a;
--border-color: #363e46;
}
.dark.bg-oled {
--bg-primary: #000000;
--bg-secondary: #141414;
--bg-tertiary: #1f1f1f;
--text-primary: #ffffff;
--text-secondary: #a0a0a0;
--text-muted: #707070;
--text-tertiary: #404040;
--border-color: #2a2a2a;
}
.dark.bg-slate {
--bg-primary: #0f172a;
--bg-secondary: #1e293b;
--bg-tertiary: #334155;
--text-primary: #f1f5f9;
--text-secondary: #94a3b8;
--text-muted: #64748b;
--text-tertiary: #475569;
--border-color: #334155;
}
.dark.bg-forest {
--bg-primary: #121a16;
--bg-secondary: #1c2a22;
--bg-tertiary: #2a3d30;
--text-primary: #e8f5ec;
--text-secondary: #8aa894;
--text-muted: #6a8874;
--text-tertiary: #4a6854;
--border-color: #2a3d30;
}
/* Printer card control buttons: stack only when they clip */
.printer-control-buttons-container {
container-type: inline-size;
}
@container (max-width: 220px) {
.printer-control-buttons {
flex-direction: column;
align-items: stretch;
}
.printer-control-buttons > button {
width: 100%;
}
}
/* ============================================
LAYER 2: STYLE EFFECTS
============================================ */
/* Classic - default, clean minimal shadows */
.style-classic {
/* Uses default shadows from :root and .dark */
}
/* Glow - accent-colored glow effects on cards */
.style-glow {
--card-shadow: 0 2px 8px rgba(0, 0, 0, 0.08), 0 0 25px color-mix(in srgb, var(--accent) 12%, transparent);
}
.dark.style-glow {
--card-shadow: 0 4px 20px rgba(0, 0, 0, 0.5), 0 0 40px color-mix(in srgb, var(--accent) 15%, transparent);
}
/* Vibrant - dramatic deep shadows, more contrast */
.style-vibrant {
--card-shadow: 0 8px 30px rgba(0, 0, 0, 0.15), 0 2px 8px rgba(0, 0, 0, 0.1);
}
.dark.style-vibrant {
--card-shadow: 0 10px 40px rgba(0, 0, 0, 0.6), 0 4px 12px rgba(0, 0, 0, 0.4);
}
/* ============================================
LAYER 3: ACCENT COLORS
============================================ */
.accent-green {
--accent: #00ae42;
--accent-light: #00c64d;
--accent-dark: #009438;
}
.accent-teal {
--accent: #14b8a6;
--accent-light: #2dd4bf;
--accent-dark: #0d9488;
}
.accent-blue {
--accent: #3b82f6;
--accent-light: #60a5fa;
--accent-dark: #2563eb;
}
.accent-orange {
--accent: #f97316;
--accent-light: #fb923c;
--accent-dark: #ea580c;
}
.accent-purple {
--accent: #8b5cf6;
--accent-light: #a78bfa;
--accent-dark: #7c3aed;
}
.accent-red {
--accent: #ef4444;
--accent-light: #f87171;
--accent-dark: #dc2626;
}
body {
background-color: var(--bg-primary);
color: var(--text-primary);
margin: 0;
min-height: 100vh;
transition: background-color 0.2s ease, color 0.2s ease;
}
#root {
min-height: 100vh;
}
/* Override text-white to be theme-aware */
.text-white {
color: var(--text-primary);
}
/* Smooth transitions for theme changes */
.bg-bambu-dark,
.bg-bambu-dark-secondary,
.bg-bambu-dark-tertiary,
.border-bambu-dark-tertiary {
transition: background-color 0.2s ease, border-color 0.2s ease;
}
/* Toast slide-in animation */
@keyframes slide-in {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
.animate-slide-in {
animation: slide-in 0.2s ease-out;
}
/* Theme-aware icon inversion - only invert in dark mode */
.icon-theme {
opacity: 0.5;
}
.dark .icon-theme {
filter: invert(1);
opacity: 0.4;
}
/* Green-colored icon for active status indicators */
.icon-green {
filter: invert(48%) sepia(89%) saturate(459%) hue-rotate(93deg) brightness(95%) contrast(92%);
opacity: 1;
}
/* Orange/red-colored icon for heating indicators */
.icon-heating {
filter: invert(50%) sepia(100%) saturate(1000%) hue-rotate(360deg) brightness(100%) contrast(100%);
opacity: 1;
}
/* Jogpad theme styling - darken background in dark mode */
.jogpad-theme {
/* Light mode - normal */
}
.dark .jogpad-theme {
filter: brightness(0.35) contrast(1.2);
}
/* Empty AMS slot with diagonal stripes */
.ams-empty-slot {
background: repeating-linear-gradient(
45deg,
#444,
#444 2px,
#222 2px,
#222 4px
);
}
.dark .ams-empty-slot {
background: repeating-linear-gradient(
45deg,
#555,
#555 2px,
#333 2px,
#333 4px
);
}
/* Touch manipulation to prevent zoom on double-tap */
.touch-manipulation {
touch-action: manipulation;
}
/* Safe area insets for notched devices */
.safe-area-bottom {
padding-bottom: env(safe-area-inset-bottom, 0);
}
.safe-area-top {
padding-top: env(safe-area-inset-top, 0);
}
/* Hide scrollbar but keep functionality */
.scrollbar-hide {
-ms-overflow-style: none;
scrollbar-width: none;
}
.scrollbar-hide::-webkit-scrollbar {
display: none;
}
/* Mobile drawer animation */
@keyframes slide-in-left {
from {
transform: translateX(-100%);
}
to {
transform: translateX(0);
}
}
.animate-slide-in-left {
animation: slide-in-left 0.3s ease-out;
}
/* SpoolBuddy idle scan animation: transform/opacity only for low-power devices */
@keyframes spoolbuddy-optimized-ping {
0% {
transform: scale(0.8);
opacity: 0.8;
}
80%,
100% {
transform: scale(2.2);
opacity: 0;
}
}
.spoolbuddy-optimized-ping {
will-change: auto;
contain: layout;
animation: spoolbuddy-optimized-ping 3.5s cubic-bezier(0, 0, 0.2, 1) infinite;
}
.spoolbuddy-spool-glow {
transform: scale(2.0);
will-change: auto;
}
@media (prefers-reduced-motion: reduce) {
.spoolbuddy-optimized-ping {
animation: none;
opacity: 0.25;
transform: scale(1.2);
}
.spoolbuddy-spool-glow {
transition: none;
}
}
/* SpoolBuddy quick menu slide-down */
@keyframes slide-down {
from {
transform: translateY(-100%);
}
to {
transform: translateY(0);
}
}
.animate-slide-down {
animation: slide-down 0.25s ease-out;
}
/* Card shadows - uses theme-specific shadow */
.card-shadow {
box-shadow: var(--card-shadow);
}
/* ============================================
SPOOLBUDDY KIOSK MODAL CONSTRAINTS
Cap large modals (max-w-2xl) to viewport on the
small SpoolBuddy touchscreen. Excludes smaller
modals like the slot action picker (max-w-sm).
============================================ */
[data-spoolbuddy-kiosk] .fixed .relative.max-w-2xl {
height: 90vh;
max-height: 90vh;
display: flex;
flex-direction: column;
}
[data-spoolbuddy-kiosk] .fixed .relative.max-w-2xl > div:first-child {
flex-shrink: 0;
}
[data-spoolbuddy-kiosk] .fixed .relative.max-w-2xl > div:last-child {
flex-shrink: 0;
}
[data-spoolbuddy-kiosk] .fixed .relative.max-w-2xl > div:nth-child(2) {
flex: 1 1 auto;
min-height: 0;
overflow-y: auto;
}
/* Remove inner spool grid max-height to avoid nested scrollbars */
[data-spoolbuddy-kiosk] .fixed .relative.max-w-2xl .max-h-96 {
max-height: none;
}
/* Calendar selected-day list scrollbar theming */
.calendar-scroll {
scrollbar-width: thin;
scrollbar-color: color-mix(in srgb, var(--text-muted) 60%, transparent) transparent;
}
.calendar-scroll::-webkit-scrollbar {
width: 8px;
}
.calendar-scroll::-webkit-scrollbar-track {
background: transparent;
}
.calendar-scroll::-webkit-scrollbar-thumb {
background-color: color-mix(in srgb, var(--text-muted) 60%, transparent);
border-radius: 999px;
border: 2px solid color-mix(in srgb, var(--bg-secondary) 70%, transparent);
}
.calendar-scroll::-webkit-scrollbar-thumb:hover {
background-color: color-mix(in srgb, var(--text-muted) 80%, transparent);
}
/* History row thumbnail hover-to-enlarge — desktop only (no hover on touch). */
@media (hover: hover) and (pointer: fine) {
.history-thumb-hover:hover .history-thumb-preview {
opacity: 1;
}
}