Files
bambuddy/frontend/src/index.css
T
maziggy 82d6d98266 Restore the pointer cursor on interactive controls (#2791)
Hovering most of Bambuddy gave an arrow rather than a hand. Not everywhere,
which is what made it read as sloppiness rather than a bug: the update pill was
inert while the buttons beside it were fine, a bed or nozzle tile responded but
the history-graph button in its corner did not, and dropdowns went either way
with no pattern behind it.

The pattern was there. Tailwind v3's Preflight set `button { cursor: pointer }`.
v4 dropped it to match the browser default, which for a button is `default`.
Bambuddy has been on v4 since the frontend was built, and `src/index.css` never
had a base layer restoring it, so a button only looked clickable where someone
had written `cursor-pointer` by hand. 15 of 934 had. 0 of 149 selects, and 19
of 130 checkbox/radio inputs. The 233 ad-hoc `cursor-pointer` usages are why it
looked arbitrary instead of uniformly broken.

One `@layer base` rule now covers button, select, checkbox, radio, summary and
[role=button]. base sits below utilities, so `cursor-not-allowed` and the
`disabled:cursor-*` variants still win; the `:not(:disabled)` guard catches the
disabled controls that carry no such utility. Verified against the built bundle
rather than the source -- the rule lands inside @layer base, and
`.cursor-not-allowed` is emitted after it.

Click-outside backdrops are deliberately excluded. 90 of the 96 remaining
onClick divs are `fixed inset-0` overlays; a full-screen sheet advertising
itself as a button is worse than one that says nothing. Of the rest, 50 are
stopPropagation wrappers and 3 are the temperature tiles, which already set the
cursor through `statusControlClass` -- which is exactly why those tiles worked
while the button nested inside them did not. That left two real ones: Card, now
conditional on an onClick actually being passed, and the queue card, whose
existing `sm:cursor-default` kept the desktop intent.

Separately, from the same report. FilamentHoverCard draws the slot menu twice,
and the two paths had drifted into opposite orders: Configure above Assign Spool
on an empty slot, the reverse on a filled one, so the menu reshuffled itself
depending on whether the slot held filament. Both now lead with the spool
action. Tests assert the order on each path, so one can no longer move without
the other -- checked by reinstating the old order and confirming the empty-slot
test fails.

Those buttons also used justify-center, which centred each label independently
and left the icons in a ragged column; they are justify-start now. Their hover
was a 10% opacity step that was very hard to see, now 20%. And the favourites
star previews yellow on hover, suppressed when the user lacks archives:update.
2026-08-08 10:06:45 +02:00

555 lines
13 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 *));
/* Restore the pointer cursor on interactive controls (#2791). Tailwind v3's
Preflight set `button { cursor: pointer }`; v4 dropped it to match the
browser default of `cursor: default`, so every button in the app looked
unclickable unless someone remembered to add `cursor-pointer` by hand.
Only a handful of the ~930 buttons did, which is why the UI felt
inconsistent rather than uniformly wrong.
This lives in `base`, the lowest of Tailwind's cascade layers, so the
`cursor-not-allowed` / `disabled:cursor-*` utilities dotted around the
codebase still win. The `:not(:disabled)` guard covers the elements that
are disabled without also carrying such a utility. */
@layer base {
button:not(:disabled),
select:not(:disabled),
summary,
input[type="checkbox"]:not(:disabled),
input[type="radio"]:not(:disabled),
[role="button"]:not([aria-disabled="true"]) {
cursor: pointer;
}
}
@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;
}
}