{
// PLA Basic has color_name 'Red' — should be filtered out
expect(screen.queryByText(/PLA Basic/)).toBeNull();
});
+
+ it('paints a clear filament as the checkerboard, not an invisible swatch', () => {
+ // Storing the alpha is what puts 00000000 into this list in the first place,
+ // and the hand-rolled `#${hex}` background this swatch used to build renders
+ // that as nothing at all — a fully transparent circle (#2912).
+ const clear: SpoolmanFilamentEntry[] = [
+ { ...FILAMENTS[0], id: 4, name: 'PLA Basic Clear', color_hex: '00000000', color_name: 'Clear' },
+ ];
+ render(
+
+ );
+ const swatch = screen.getByLabelText('inventory.spoolmanFilamentColorSwatch');
+ expect(swatch.style.backgroundImage).toContain('repeating-conic-gradient');
+ expect(swatch.style.backgroundColor).toBe('');
+ });
});
diff --git a/frontend/src/__tests__/utils/colors.test.ts b/frontend/src/__tests__/utils/colors.test.ts
index 8162e94a8..17f3370fc 100644
--- a/frontend/src/__tests__/utils/colors.test.ts
+++ b/frontend/src/__tests__/utils/colors.test.ts
@@ -1,6 +1,7 @@
import { describe, it, expect, beforeEach } from 'vitest';
import {
disambiguateColorNames,
+ getSwatchStyle,
colorFamily,
colorSortKey,
hexToColorName,
@@ -343,3 +344,48 @@ describe('disambiguateColorNames', () => {
expect(disambiguateColorNames({}, {})).toEqual(['', '']);
});
});
+
+
+describe('getSwatchStyle (#1545, #2912)', () => {
+ const CHECKERBOARD = 'repeating-conic-gradient(#979797 0% 25%, #f5f5f5 0% 50%)';
+
+ it('falls back to neutral grey for missing or unparseable input', () => {
+ expect(getSwatchStyle(null)).toEqual({ backgroundColor: '#808080' });
+ expect(getSwatchStyle(undefined)).toEqual({ backgroundColor: '#808080' });
+ expect(getSwatchStyle('')).toEqual({ backgroundColor: '#808080' });
+ expect(getSwatchStyle('ABC')).toEqual({ backgroundColor: '#808080' });
+ });
+
+ it('paints an opaque colour flat, with or without the FF byte', () => {
+ expect(getSwatchStyle('FF0000')).toEqual({ backgroundColor: '#FF0000' });
+ expect(getSwatchStyle('FF0000FF')).toEqual({ backgroundColor: '#FF0000' });
+ expect(getSwatchStyle('#FF0000FF')).toEqual({ backgroundColor: '#FF0000' });
+ });
+
+ it('shows the checkerboard alone for a fully transparent colour', () => {
+ expect(getSwatchStyle('00000000')).toEqual({
+ backgroundImage: CHECKERBOARD,
+ backgroundSize: '8px 8px',
+ });
+ });
+
+ it('layers a partly translucent colour over the checkerboard (#2912)', () => {
+ // Regression: this used to fall through to the RGB prefix, so a 50%-alpha
+ // spool rendered identically to an opaque one. Spoolman mode can now store
+ // any non-FF alpha, so the in-between case is reachable in normal use.
+ const style = getSwatchStyle('FF000080');
+ expect(style.backgroundColor).toBeUndefined();
+ expect(style.backgroundImage).toBe(
+ `linear-gradient(#FF000080, #FF000080), ${CHECKERBOARD}`,
+ );
+ expect(style.backgroundSize).toBe('100% 100%, 8px 8px');
+ });
+
+ it('treats the alpha byte case-insensitively', () => {
+ expect(getSwatchStyle('ff0000ff')).toEqual({ backgroundColor: '#ff0000' });
+ expect(getSwatchStyle('ff000000')).toEqual({
+ backgroundImage: CHECKERBOARD,
+ backgroundSize: '8px 8px',
+ });
+ });
+});
diff --git a/frontend/src/components/AssignSpoolModal.tsx b/frontend/src/components/AssignSpoolModal.tsx
index f4045ff3b..f88af20b4 100644
--- a/frontend/src/components/AssignSpoolModal.tsx
+++ b/frontend/src/components/AssignSpoolModal.tsx
@@ -381,7 +381,7 @@ export function AssignSpoolModal({ isOpen, onClose, printerId, amsId, trayId, tr
{trayInfo.color && (
)}
{trayInfo.type || t('ams.emptySlot')}
diff --git a/frontend/src/components/ConfigureAmsSlotModal.tsx b/frontend/src/components/ConfigureAmsSlotModal.tsx
index 691d48d38..b1c47f68b 100644
--- a/frontend/src/components/ConfigureAmsSlotModal.tsx
+++ b/frontend/src/components/ConfigureAmsSlotModal.tsx
@@ -8,6 +8,7 @@ import { matchesPrinterModelSuffix, presetCompatibility, buildCompatibilityIndex
import { toFilamentId } from './spool-form/utils';
import { Button } from './Button';
import { getAmsLabel } from '../utils/amsHelpers';
+import { getSwatchStyle } from '../utils/colors';
import { useCancellableTimeout } from '../hooks/useCancellableTimeout';
interface SlotInfo {
@@ -1179,7 +1180,10 @@ export function ConfigureAmsSlotModal({
const canSave = selectedPresetId && !configureMutation.isPending;
// Get display color (custom or slot default)
- const displayColor = colorHex || slotInfo.trayColor?.slice(0, 6) || 'FFFFFF';
+ // Not sliced to six: a clear tray reports RRGGBB00, and cutting the alpha off
+ // here previewed it as solid black. `colorHex` is the edited form value and is
+ // always six characters, so only the tray fallback ever carries an alpha (#2912).
+ const displayColor = colorHex || slotInfo.trayColor || 'FFFFFF';
return (