+
+
+ @if (criticalCount() > 0) {
+
+ error
+ {{ criticalCount() }} spool{{ criticalCount() > 1 ? 's' : '' }} critically low (≤10% remaining)
+
+ } @else if (lowStockCount() > 0) {
+
+ warning
+ {{ lowStockCount() }} spool{{ lowStockCount() > 1 ? 's' : '' }} running low (≤25% remaining)
+
+ }
+
+
+
+
+
+
+ | Color |
+
+
+
+ |
+
+
+
+
+ Material |
+
+ {{ filament.materialBaseName }}
+ @if (filament.materialModifierName) {
+ {{ filament.materialModifierName }}
+ }
+ |
+
+
+
+
+ Brand |
+ {{ filament.brand }} |
+
+
+
+
+ Serial |
+ {{ filament.spoolSerial }} |
+
+
+
+
+ Remaining |
+
+
+
+ {{ formatWeight(filament.weightRemainingGrams) }} / {{ formatWeight(filament.weightTotalGrams) }}
+
+
+
+
+ |
+
+
+
+
+ Stock |
+
+ @let level = classifyStockLevel(filament);
+
+
+ {{ stockLevelIcon(level) }}
+ {{ stockLevelLabel(level) }}
+
+
+ |
+
+
+
+
+ Status |
+
+
+ {{ filament.isActive ? 'Active' : 'Inactive' }}
+
+ |
+
+
+
+
+
+
+
+
+ @if (filaments().length === 0) {
+
+
inventory_2
+
No filament spools found
+
+ }
+
\ No newline at end of file
diff --git a/frontend/src/app/components/filament-table/filament-table.component.scss b/frontend/src/app/components/filament-table/filament-table.component.scss
new file mode 100644
index 0000000..d85bb4a
--- /dev/null
+++ b/frontend/src/app/components/filament-table/filament-table.component.scss
@@ -0,0 +1,259 @@
+/**
+ * Filament Table Component Styles
+ * Touch-optimized for kiosk (Raspberry Pi 5) and mobile PWA
+ * Low stock indicators use high-contrast colors for workshop visibility
+ */
+
+// Touch-optimized sizing
+$touch-target-min: 48px;
+$spacing-unit: 8px;
+
+// Stock level colors — high contrast, accessible
+$color-critical: #ef4444; // Red — critically low
+$color-low: #f59e0b; // Amber — running low
+$color-moderate: #3b82f6; // Blue — moderate
+$color-healthy: #22c55e; // Green — healthy/OK
+$color-active: #22c55e; // Green — active spool
+$color-inactive: #94a3b8; // Gray — inactive spool
+
+.filament-table-container {
+ width: 100%;
+ overflow-x: auto;
+ -webkit-overflow-scrolling: touch;
+}
+
+// Alert banner for low stock warnings
+.alert-banner {
+ display: flex;
+ align-items: center;
+ gap: $spacing-unit;
+ padding: $spacing-unit * 1.5 $spacing-unit * 2;
+ border-radius: 8px;
+ margin-bottom: $spacing-unit * 2;
+ font-size: 14px;
+ font-weight: 500;
+
+ mat-icon {
+ font-size: 20px !important;
+ width: 20px !important;
+ height: 20px !important;
+ }
+
+ &.critical {
+ background-color: rgba($color-critical, 0.12);
+ color: $color-critical;
+ border: 1px solid rgba($color-critical, 0.3);
+ }
+
+ &.low {
+ background-color: rgba($color-low, 0.12);
+ color: $color-low;
+ border: 1px solid rgba($color-low, 0.3);
+ }
+}
+
+// Table styling
+.filament-table {
+ width: 100%;
+ min-width: 700px;
+
+ th {
+ font-weight: 600;
+ font-size: 13px;
+ text-transform: uppercase;
+ letter-spacing: 0.04em;
+ color: var(--mat-sys-on-surface-variant);
+ }
+
+ td {
+ font-size: 14px;
+ padding: 12px 16px !important;
+ min-height: $touch-target-min;
+
+ @media (max-width: 480px) {
+ padding: 8px 12px !important;
+ font-size: 13px;
+ }
+ }
+
+ // Row highlight for low stock
+ .mat-mdc-row {
+ transition: background-color 0.2s ease;
+ }
+
+ &.row-critical {
+ background-color: rgba($color-critical, 0.06) !important;
+
+ &:hover {
+ background-color: rgba($color-critical, 0.1) !important;
+ }
+ }
+
+ &.row-low {
+ background-color: rgba($color-low, 0.06) !important;
+
+ &:hover {
+ background-color: rgba($color-low, 0.1) !important;
+ }
+ }
+}
+
+// Color swatch
+.color-swatch {
+ display: inline-block;
+ width: 28px;
+ height: 28px;
+ border-radius: 50%;
+ border: 2px solid rgba(0, 0, 0, 0.12);
+ vertical-align: middle;
+ cursor: default;
+
+ @media (max-width: 480px) {
+ width: 24px;
+ height: 24px;
+ }
+}
+
+// Material name
+.material-name {
+ font-weight: 500;
+}
+
+.material-modifier {
+ font-size: 12px;
+ color: var(--mat-sys-on-surface-variant);
+ margin-left: 4px;
+}
+
+// Serial cell — monospace
+.serial-cell {
+ font-family: 'JetBrains Mono', 'Roboto Mono', monospace;
+ font-size: 13px;
+ letter-spacing: 0.02em;
+}
+
+// Remaining weight cell
+.remaining-cell {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+ min-width: 120px;
+
+ .remaining-text {
+ font-size: 13px;
+ color: var(--mat-sys-on-surface-variant);
+ }
+}
+
+// Progress bar stock level variants
+mat-progress-bar {
+ &.critical {
+ --mat-progress-bar-active-indicator-color: #{$color-critical};
+ }
+
+ &.low {
+ --mat-progress-bar-active-indicator-color: #{$color-low};
+ }
+
+ &.moderate {
+ --mat-progress-bar-active-indicator-color: #{$color-moderate};
+ }
+
+ &.healthy {
+ --mat-progress-bar-active-indicator-color: #{$color-healthy};
+ }
+}
+
+// Stock level chip variants
+mat-chip {
+ min-height: 32px !important;
+ font-size: 12px !important;
+
+ &.critical {
+ background-color: rgba($color-critical, 0.15) !important;
+ color: $color-critical;
+
+ mat-icon {
+ color: $color-critical;
+ }
+ }
+
+ &.low {
+ background-color: rgba($color-low, 0.15) !important;
+ color: $color-low;
+
+ mat-icon {
+ color: $color-low;
+ }
+ }
+
+ &.moderate {
+ background-color: rgba($color-moderate, 0.1) !important;
+ color: $color-moderate;
+
+ mat-icon {
+ color: $color-moderate;
+ }
+ }
+
+ &.healthy {
+ background-color: rgba($color-healthy, 0.1) !important;
+ color: $color-healthy;
+
+ mat-icon {
+ color: $color-healthy;
+ }
+ }
+
+ mat-icon {
+ font-size: 16px !important;
+ width: 16px !important;
+ height: 16px !important;
+ margin-right: 4px;
+ }
+}
+
+// Status badge
+.status-badge {
+ display: inline-flex;
+ align-items: center;
+ padding: 4px 12px;
+ border-radius: 12px;
+ font-size: 12px;
+ font-weight: 600;
+ text-transform: uppercase;
+ letter-spacing: 0.04em;
+
+ &.active {
+ background-color: rgba($color-active, 0.12);
+ color: $color-active;
+ }
+
+ &.inactive {
+ background-color: rgba($color-inactive, 0.12);
+ color: $color-inactive;
+ }
+}
+
+// Empty state
+.empty-state {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ padding: 48px $spacing-unit * 2;
+ color: var(--mat-sys-on-surface-variant);
+
+ mat-icon {
+ font-size: 48px !important;
+ width: 48px !important;
+ height: 48px !important;
+ opacity: 0.4;
+ margin-bottom: $spacing-unit * 2;
+ }
+
+ p {
+ font-size: 16px;
+ margin: 0;
+ }
+}
\ No newline at end of file
diff --git a/frontend/src/app/components/filament-table/filament-table.component.spec.ts b/frontend/src/app/components/filament-table/filament-table.component.spec.ts
new file mode 100644
index 0000000..53cd79e
--- /dev/null
+++ b/frontend/src/app/components/filament-table/filament-table.component.spec.ts
@@ -0,0 +1,88 @@
+import { describe, it, expect } from 'vitest';
+import {
+ Filament,
+ StockLevel,
+ getRemainingPercent,
+ classifyStockLevel,
+} from '../../models/filament.model';
+
+/** Create a test filament with defaults — override specific fields */
+function createFilament(overrides: Partial