mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-08 23:21:58 +02:00
fix(ui): resolve light-theme low-contrast semantic text app-wide (#1909)
The app was built dark-first, so hundreds of hardcoded Tailwind semantic text/icon utilities at light shades (text-amber-400, text-blue-300, ...) had no dark: variant. With darkMode:'class' they applied in light theme too, producing washed-out text on pale tints and white cards — including the three reported spots (AMS Drying banner, Archives no-3MF warning, debug-logging banner). Give each a theme-aware pair: a darker readable shade in light theme with the original pinned to dark:, so dark theme is unchanged. ~100 files. The bambu-* CSS-variable palette (self-correcting) and the dark-only SpoolBuddy kiosk are left untouched. Plain text-white is already theme-aware via the existing index.css .text-white override, so it needed no changes.
This commit is contained in:
@@ -5,6 +5,7 @@ All notable changes to Bambuddy will be documented in this file.
|
||||
## [0.2.5b2] - Unreleased
|
||||
|
||||
### Fixed
|
||||
- **Light theme: low-contrast washed-out text on status/warning banners and colored badges (#1909, reporter @AntonPalmqvist)** — The app was built dark-first: hundreds of hardcoded Tailwind semantic-color text utilities at light shades (`text-amber-400`, `text-blue-300`, `text-green-200`, etc.) had no `dark:` variant, so with `darkMode: 'class'` they applied in the light theme too — light-amber/blue/green text on pale `bg-*-500/10` tints or white cards, barely readable. The three reporter-cited spots were all this bug: the AMS **Drying** status banner (Printers page), the **Archives** no-3MF-thumbnail warning, and the **System/debug-logging** banner. Fixed app-wide by giving every affected semantic text/icon/tint a theme-aware pair — a readable darker shade in light theme (e.g. `text-amber-700`/`-800`/`-900`) with the original light shade pinned to `dark:` so the dark theme is unchanged — across ~100 files (banners, status pills, permission/role badges, method chips, validation errors, stat-card icons, delete-action hovers, and more). The custom CSS-variable `bambu-*` palette (which already self-corrects per theme) and the SpoolBuddy kiosk (dark-theme-only) were deliberately left untouched.
|
||||
- **Sponsor toast ignored its 14-day cooldown and re-fired on every fresh session (#2477, reporter @pchulpjoost)** — The in-app supporter toast ("You've completed X prints with Bambuddy…") reappeared on essentially every visit instead of respecting the documented 14-day cross-family cooldown. **Root cause:** the backend owns the cooldown, but it only persists the cooldown anchor (`last_shown_at`) and the seen-milestone record inside `POST /sponsor-prompt/dismiss` — and the frontend hook only called `dismiss` from the "View supporters" CTA's `onClick`. A user who *saw* the toast but never clicked the CTA persisted no state at all. The per-tab `sessionStorage` guard hid the re-fire within a single browser session, but every fresh session (new tab, reopened browser, or another device — the reporter confirmed "Other OS/Browser has the same problem") re-ran the check against still-empty backend state and showed the same milestone again. To a user opening Bambuddy to check on a finished print, that looked like "every print." **Fix:** the hook now records the toast as shown the moment it renders — it POSTs `/dismiss` (whose documented job is exactly "anchor the 14-day cooldown and record the milestone as shown") immediately after `showPersistentToast`, instead of waiting for a CTA click. Being *displayed* is what arms the cooldown; clicking the CTA stays optional and just navigates to the sponsors page. `frontend/src/hooks/useSponsorPrompt.ts` only; backend cooldown logic unchanged.
|
||||
- **Windows installer: fresh install fails to start — "connection refused", nothing listening on port 8000 (#2474, reporter @fangme)** — On a clean Windows 10 machine, installing the `.exe` left the Bambuddy service showing "running" in services.msc but the dashboard refused every connection (localhost, IP, and hostname), and `netstat -ano` showed nothing on :8000. **Root cause — bottom of a 100k-line `service-stderr.log` traceback:** `init_db()` → SQLAlchemy async engine → `greenlet_spawn` → `ValueError: the greenlet library is required to use this function. DLL load failed while importing _greenlet: The specified module could not be found.` The 100k repeated `merged_lifespan` frames above it are just FastAPI's nested-lifespan stack unwinding — noise. The real failure: greenlet's `_greenlet.pyd` couldn't load, SQLAlchemy's async engine couldn't start, the FastAPI lifespan raised, and uvicorn **never bound the port** — so the supervised process stays up (NSSM sees it running) while the app itself has crashed on startup. **Why greenlet specifically:** the Win32 error 126 ("The specified module could not be found") on a `.pyd` that pip installed successfully means the module is present but a *dependency DLL* is missing. greenlet's extension is **C++** and needs `vcruntime140_1.dll` (table-based exception handling); the python.org **embeddable** distribution the installer bundles ships `vcruntime140.dll` but **not** `vcruntime140_1.dll`. `python313.dll` is pure C and only needs the former, which is exactly why python.exe starts and runs all the way to `init_db` before greenlet is the first thing to need the missing C++ runtime. On machines that already have the VC++ 2015-2022 redistributable installed (the maintainer's box, CI runners) `vcruntime140_1.dll` is in System32 and everything works — masking the bug until a truly fresh Win10 box hit it. **Fix:** the installer build now ships the C++ runtime app-locally — `vcruntime140_1.dll` and `msvcp140.dll` are staged next to `python.exe` (where `vcruntime140.dll` already lives), sourced from a vendored copy if present or the build runner's System32 otherwise, failing the build loudly if neither has them. `installers/windows/build.py` only; the Inno Setup `[Files]` step already copies `staging\python\*` recursively so the new DLLs are packaged automatically. **Workaround for the current build:** install the "Microsoft Visual C++ 2015-2022 Redistributable (x64)" from Microsoft, then restart the Bambuddy service — that puts `vcruntime140_1.dll` in System32 where the embedded Python finds it.
|
||||
- **Virtual Printer "bind interface" dropdown is empty on macOS** — Adding a Virtual Printer on macOS showed no interfaces to bind to. `get_network_interfaces()` only routed Windows to the cross-platform psutil path; macOS fell into the Linux branch, which uses the Linux-only `SIOCGIFADDR`/`SIOCGIFNETMASK` ioctls (`0x8915`/`0x891B`). macOS/BSD have `fcntl` but different ioctl numbers and sockaddr layout, so every per-interface ioctl raised `OSError` and the function silently returned an empty list (and `get_all_interface_ips()`, which has no `ip` binary to fall back to on macOS, inherited the empty result). Interface enumeration now routes **all** non-Linux platforms (macOS, BSD, Windows) through psutil, which returns each interface's name + IPv4 + netmask and filters loopback/link-local/down adapters while keeping real LAN and VPN (utun/Tailscale) interfaces bindable. Linux keeps its existing ioctl path unchanged.
|
||||
|
||||
@@ -136,8 +136,8 @@ export function AMSHistoryModal({
|
||||
const tempTrend = getTrend(chartData.map(d => d.temperature));
|
||||
|
||||
const TrendIcon = ({ trend }: { trend: string }) => {
|
||||
if (trend === 'up') return <TrendingUp className="w-4 h-4 text-red-400" />;
|
||||
if (trend === 'down') return <TrendingDown className="w-4 h-4 text-green-400" />;
|
||||
if (trend === 'up') return <TrendingUp className="w-4 h-4 text-red-600 dark:text-red-400" />;
|
||||
if (trend === 'down') return <TrendingDown className="w-4 h-4 text-green-600 dark:text-green-400" />;
|
||||
return <Minus className="w-4 h-4 text-gray-400 dark:text-bambu-gray" />;
|
||||
};
|
||||
|
||||
|
||||
@@ -70,11 +70,11 @@ interface APIResponse {
|
||||
}
|
||||
|
||||
const METHOD_COLORS: Record<string, string> = {
|
||||
get: 'bg-blue-500/20 text-blue-400 border-blue-500/30',
|
||||
post: 'bg-green-500/20 text-green-400 border-green-500/30',
|
||||
put: 'bg-yellow-500/20 text-yellow-400 border-yellow-500/30',
|
||||
patch: 'bg-orange-500/20 text-orange-400 border-orange-500/30',
|
||||
delete: 'bg-red-500/20 text-red-400 border-red-500/30',
|
||||
get: 'bg-blue-100 dark:bg-blue-500/20 text-blue-700 dark:text-blue-400 border-blue-300 dark:border-blue-500/30',
|
||||
post: 'bg-green-100 dark:bg-green-500/20 text-green-700 dark:text-green-400 border-green-300 dark:border-green-500/30',
|
||||
put: 'bg-yellow-100 dark:bg-yellow-500/20 text-yellow-700 dark:text-yellow-400 border-yellow-300 dark:border-yellow-500/30',
|
||||
patch: 'bg-orange-100 dark:bg-orange-500/20 text-orange-700 dark:text-orange-400 border-orange-300 dark:border-orange-500/30',
|
||||
delete: 'bg-red-100 dark:bg-red-500/20 text-red-700 dark:text-red-400 border-red-300 dark:border-red-500/30',
|
||||
};
|
||||
|
||||
function resolveRef(schema: OpenAPISchema, ref: string): SchemaSpec {
|
||||
@@ -340,7 +340,7 @@ function EndpointItem({ path, method, spec, schema, apiKey }: EndpointItemProps)
|
||||
<div key={param.name} className="flex items-center gap-2">
|
||||
<label className="text-sm text-bambu-gray w-32 flex-shrink-0">
|
||||
{param.name}
|
||||
{param.required && <span className="text-red-400 ml-1">*</span>}
|
||||
{param.required && <span className="text-red-700 dark:text-red-400 ml-1">*</span>}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
@@ -364,7 +364,7 @@ function EndpointItem({ path, method, spec, schema, apiKey }: EndpointItemProps)
|
||||
<div key={param.name} className="flex items-center gap-2">
|
||||
<label className="text-sm text-bambu-gray w-32 flex-shrink-0">
|
||||
{param.name}
|
||||
{param.required && <span className="text-red-400 ml-1">*</span>}
|
||||
{param.required && <span className="text-red-700 dark:text-red-400 ml-1">*</span>}
|
||||
</label>
|
||||
{param.schema?.enum ? (
|
||||
<select
|
||||
@@ -417,7 +417,7 @@ function EndpointItem({ path, method, spec, schema, apiKey }: EndpointItemProps)
|
||||
Execute
|
||||
</Button>
|
||||
{missingParams.length > 0 && (
|
||||
<span className="text-xs text-yellow-400 flex items-center gap-1">
|
||||
<span className="text-xs text-yellow-700 dark:text-yellow-400 flex items-center gap-1">
|
||||
<AlertCircle className="w-3 h-3" />
|
||||
Fill in: {missingParams.join(', ')}
|
||||
</span>
|
||||
@@ -432,10 +432,10 @@ function EndpointItem({ path, method, spec, schema, apiKey }: EndpointItemProps)
|
||||
Response
|
||||
<span className={`px-2 py-0.5 text-xs rounded ${
|
||||
response.status >= 200 && response.status < 300
|
||||
? 'bg-green-500/20 text-green-400'
|
||||
? 'bg-green-100 dark:bg-green-500/20 text-green-700 dark:text-green-400'
|
||||
: response.status >= 400
|
||||
? 'bg-red-500/20 text-red-400'
|
||||
: 'bg-yellow-500/20 text-yellow-400'
|
||||
? 'bg-red-100 dark:bg-red-500/20 text-red-700 dark:text-red-400'
|
||||
: 'bg-yellow-100 dark:bg-yellow-500/20 text-yellow-700 dark:text-yellow-400'
|
||||
}`}>
|
||||
{response.status} {response.statusText}
|
||||
</span>
|
||||
@@ -443,7 +443,7 @@ function EndpointItem({ path, method, spec, schema, apiKey }: EndpointItemProps)
|
||||
</h4>
|
||||
<Button variant="secondary" size="sm" onClick={copyResponse}>
|
||||
{copied ? (
|
||||
<CheckCircle className="w-3 h-3 text-green-400" />
|
||||
<CheckCircle className="w-3 h-3 text-green-600 dark:text-green-400" />
|
||||
) : (
|
||||
<Copy className="w-3 h-3" />
|
||||
)}
|
||||
@@ -501,7 +501,7 @@ export function APIBrowser({ apiKey = '' }: APIBrowserProps) {
|
||||
return (
|
||||
<Card>
|
||||
<CardContent className="py-8">
|
||||
<div className="text-center text-red-400">
|
||||
<div className="text-center text-red-700 dark:text-red-400">
|
||||
<AlertCircle className="w-12 h-12 mx-auto mb-3 opacity-50" />
|
||||
<p>Failed to load API schema</p>
|
||||
<p className="text-sm text-bambu-gray mt-1">{error}</p>
|
||||
|
||||
@@ -187,7 +187,7 @@ export function AddExternalLinkModal({ link, onClose }: AddExternalLinkModalProp
|
||||
{/* Form */}
|
||||
<form onSubmit={handleSubmit} className="p-6 space-y-4">
|
||||
{error && (
|
||||
<div className="p-3 bg-red-500/20 border border-red-500/50 rounded-lg text-sm text-red-400">
|
||||
<div className="p-3 bg-red-100 dark:bg-red-500/20 border border-red-300 dark:border-red-500/50 rounded-lg text-sm text-red-700 dark:text-red-400">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
@@ -256,7 +256,7 @@ export function AddExternalLinkModal({ link, onClose }: AddExternalLinkModalProp
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleRemoveCustomIcon}
|
||||
className="p-1 text-red-400 hover:text-red-300 transition-colors"
|
||||
className="p-1 text-red-700 dark:text-red-400 hover:text-red-800 dark:hover:text-red-300 transition-colors"
|
||||
title="Remove custom icon"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
|
||||
@@ -284,7 +284,7 @@ export function AddNotificationModal({ provider, onClose }: AddNotificationModal
|
||||
{/* Form */}
|
||||
<form onSubmit={handleSubmit} className="p-6 space-y-4">
|
||||
{error && (
|
||||
<div className="p-3 bg-red-500/20 border border-red-500/50 rounded-lg text-sm text-red-400">
|
||||
<div className="p-3 bg-red-100 dark:bg-red-500/20 border border-red-300 dark:border-red-500/50 rounded-lg text-sm text-red-700 dark:text-red-400">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
@@ -392,7 +392,7 @@ export function AddNotificationModal({ provider, onClose }: AddNotificationModal
|
||||
<div className={`p-3 rounded-lg flex items-center gap-2 ${
|
||||
testResult.success
|
||||
? 'bg-bambu-green/20 border border-bambu-green/50 text-bambu-green'
|
||||
: 'bg-red-500/20 border border-red-500/50 text-red-400'
|
||||
: 'bg-red-100 dark:bg-red-500/20 border border-red-300 dark:border-red-500/50 text-red-700 dark:text-red-400'
|
||||
}`}>
|
||||
{testResult.success ? (
|
||||
<>
|
||||
|
||||
@@ -431,7 +431,7 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) {
|
||||
{/* Form */}
|
||||
<form onSubmit={handleSubmit} className="p-6 space-y-4 overflow-y-auto">
|
||||
{error && (
|
||||
<div className="p-3 bg-red-500/20 border border-red-500/50 rounded-lg text-sm text-red-400">
|
||||
<div className="p-3 bg-red-100 dark:bg-red-500/20 border border-red-400 dark:border-red-500/50 rounded-lg text-sm text-red-700 dark:text-red-400">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
@@ -585,7 +585,7 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) {
|
||||
{/* HA not configured */}
|
||||
{!haConfigured && (
|
||||
<div className="space-y-3">
|
||||
<div className="p-3 bg-yellow-500/20 border border-yellow-500/50 rounded-lg text-sm text-yellow-400">
|
||||
<div className="p-3 bg-yellow-100 dark:bg-yellow-500/20 border border-yellow-400 dark:border-yellow-500/50 rounded-lg text-sm text-yellow-700 dark:text-yellow-400">
|
||||
{t('smartPlugs.haNotConfigured')}{' '}
|
||||
<span className="font-medium">{t('smartPlugs.haSettingsPath')}</span>
|
||||
</div>
|
||||
@@ -612,7 +612,7 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) {
|
||||
)}
|
||||
|
||||
{haEntitiesError && (
|
||||
<div className="p-3 bg-red-500/20 border border-red-500/50 rounded-lg text-sm text-red-400">
|
||||
<div className="p-3 bg-red-100 dark:bg-red-500/20 border border-red-400 dark:border-red-500/50 rounded-lg text-sm text-red-700 dark:text-red-400">
|
||||
{t('smartPlugs.failedToLoadEntities', { error: (haEntitiesError as Error).message })}
|
||||
</div>
|
||||
)}
|
||||
@@ -988,7 +988,7 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) {
|
||||
<div className="space-y-3">
|
||||
{/* MQTT broker not configured */}
|
||||
{!settings?.mqtt_broker && (
|
||||
<div className="p-3 bg-yellow-500/20 border border-yellow-500/50 rounded-lg text-sm text-yellow-400">
|
||||
<div className="p-3 bg-yellow-100 dark:bg-yellow-500/20 border border-yellow-400 dark:border-yellow-500/50 rounded-lg text-sm text-yellow-700 dark:text-yellow-400">
|
||||
{t('smartPlugs.mqttNotConfigured')}{' '}
|
||||
<span className="font-medium">{t('smartPlugs.mqttSettingsPath')}</span>
|
||||
{' '}{t('smartPlugs.mqttNotConfiguredSuffix')}
|
||||
@@ -998,7 +998,7 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) {
|
||||
{/* MQTT broker configured - show fields */}
|
||||
{settings?.mqtt_broker && (
|
||||
<>
|
||||
<div className="p-3 bg-blue-500/10 border border-blue-500/30 rounded-lg text-sm text-blue-300">
|
||||
<div className="p-3 bg-blue-50 dark:bg-blue-500/10 border border-blue-300 dark:border-blue-500/30 rounded-lg text-sm text-blue-700 dark:text-blue-300">
|
||||
<p className="font-medium mb-1">{t('smartPlugs.monitorOnly')}</p>
|
||||
<p className="text-xs opacity-80">
|
||||
{t('smartPlugs.mqttMonitorOnlyDescription')}
|
||||
@@ -1346,7 +1346,7 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) {
|
||||
</div>
|
||||
)}
|
||||
{testResult && (
|
||||
<div className={`flex items-center gap-2 text-sm ${testResult.success ? 'text-green-400' : 'text-red-400'}`}>
|
||||
<div className={`flex items-center gap-2 text-sm ${testResult.success ? 'text-green-700 dark:text-green-400' : 'text-red-700 dark:text-red-400'}`}>
|
||||
{testResult.success ? <CheckCircle className="w-4 h-4" /> : <WifiOff className="w-4 h-4" />}
|
||||
{testResult.success ? t('smartPlugs.connectionSuccess') : t('smartPlugs.addSmartPlug.connectionFailed')}
|
||||
</div>
|
||||
@@ -1391,7 +1391,7 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) {
|
||||
<div className={`p-3 rounded-lg flex items-center gap-2 ${
|
||||
testResult.success
|
||||
? 'bg-bambu-green/20 border border-bambu-green/50 text-bambu-green'
|
||||
: 'bg-red-500/20 border border-red-500/50 text-red-400'
|
||||
: 'bg-red-100 dark:bg-red-500/20 border border-red-400 dark:border-red-500/50 text-red-700 dark:text-red-400'
|
||||
}`}>
|
||||
{testResult.success ? (
|
||||
<>
|
||||
|
||||
@@ -42,7 +42,7 @@ export function AlertModal({
|
||||
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||
}, [onClose]);
|
||||
|
||||
const iconColor = variant === 'warning' ? 'text-yellow-400' : 'text-red-400';
|
||||
const iconColor = variant === 'warning' ? 'text-yellow-600 dark:text-yellow-400' : 'text-red-600 dark:text-red-400';
|
||||
|
||||
return (
|
||||
<div
|
||||
|
||||
@@ -56,7 +56,7 @@ export function ApiKeyQRCodeModal({ apiKey, baseUrl, onClose }: ApiKeyQRCodeModa
|
||||
<div className="bg-white p-4 rounded-lg mb-4">
|
||||
<QRCodeSVG value={payload} size={256} />
|
||||
</div>
|
||||
<div className="flex items-start gap-2 text-xs text-amber-400 mb-4">
|
||||
<div className="flex items-start gap-2 text-xs text-amber-700 dark:text-amber-400 mb-4">
|
||||
<AlertTriangle className="w-4 h-4 flex-shrink-0 mt-0.5" />
|
||||
<span>{t('settings.apiKeyQrWarning')}</span>
|
||||
</div>
|
||||
|
||||
@@ -572,7 +572,7 @@ export function AssignSpoolModal({ isOpen, onClose, printerId, amsId, trayId, tr
|
||||
|
||||
|
||||
{assignMutation.isError && (
|
||||
<div className="mx-4 mb-4 p-2 bg-red-500/20 border border-red-500/50 rounded text-sm text-red-400">
|
||||
<div className="mx-4 mb-4 p-2 bg-red-100 dark:bg-red-500/20 border border-red-300 dark:border-red-500/50 rounded text-sm text-red-700 dark:text-red-400">
|
||||
{(assignMutation.error as Error).message}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -271,10 +271,10 @@ export function BackupModal({ onClose, onExport }: BackupModalProps) {
|
||||
|
||||
{/* Archive warning */}
|
||||
{selected.archives && (
|
||||
<div className="mx-4 mb-2 p-3 rounded-lg bg-yellow-500/10 border border-yellow-500/30">
|
||||
<div className="mx-4 mb-2 p-3 rounded-lg bg-yellow-50 dark:bg-yellow-500/10 border border-yellow-300 dark:border-yellow-500/30">
|
||||
<div className="flex items-start gap-2 text-sm">
|
||||
<Archive className="w-4 h-4 text-yellow-500 mt-0.5 flex-shrink-0" />
|
||||
<div className="text-yellow-200 dark:text-yellow-200 text-yellow-700">
|
||||
<div className="text-yellow-700 dark:text-yellow-200">
|
||||
<span className="font-medium">ZIP file will be created.</span>
|
||||
<span className="text-yellow-600 dark:text-yellow-200/70"> Includes all 3MF files, thumbnails, timelapses, and photos. This may take a while and result in a large file.</span>
|
||||
</div>
|
||||
@@ -296,7 +296,7 @@ export function BackupModal({ onClose, onExport }: BackupModalProps) {
|
||||
<Toggle checked={includeAccessCodes} onChange={setIncludeAccessCodes} />
|
||||
</div>
|
||||
{includeAccessCodes && (
|
||||
<div className="mt-2 p-2 rounded bg-orange-500/10 border border-orange-500/30">
|
||||
<div className="mt-2 p-2 rounded bg-orange-50 dark:bg-orange-500/10 border border-orange-300 dark:border-orange-500/30">
|
||||
<div className="flex items-start gap-2 text-xs">
|
||||
<AlertTriangle className="w-3 h-3 text-orange-500 dark:text-orange-400 mt-0.5 flex-shrink-0" />
|
||||
<span className="text-orange-700 dark:text-orange-200">
|
||||
|
||||
@@ -129,7 +129,7 @@ export function BatchProjectModal({ selectedIds, onClose }: BatchProjectModalPro
|
||||
className="w-full flex items-center gap-3 p-3 rounded-lg bg-bambu-dark hover:bg-bambu-dark-tertiary border border-bambu-dark-tertiary transition-colors text-left disabled:opacity-50"
|
||||
>
|
||||
<div className="w-8 h-8 rounded-full bg-red-500/20 flex items-center justify-center shrink-0">
|
||||
<XCircle className="w-4 h-4 text-red-400" />
|
||||
<XCircle className="w-4 h-4 text-red-600 dark:text-red-400" />
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="text-white font-medium">Remove from project</p>
|
||||
|
||||
@@ -187,7 +187,7 @@ export function BatchTagModal({ selectedIds, existingTags, onClose }: BatchTagMo
|
||||
<span
|
||||
key={tag}
|
||||
className={`px-2 py-1 rounded text-sm flex items-center gap-1 ${
|
||||
mode === 'add' ? 'bg-green-500/20 text-green-400' : 'bg-red-500/20 text-red-400'
|
||||
mode === 'add' ? 'bg-green-100 dark:bg-green-500/20 text-green-700 dark:text-green-400' : 'bg-red-100 dark:bg-red-500/20 text-red-700 dark:text-red-400'
|
||||
}`}
|
||||
>
|
||||
{tag}
|
||||
|
||||
@@ -417,7 +417,7 @@ export function BulkEditSpoolsModal({
|
||||
/>
|
||||
</div>
|
||||
{isInvalid && (
|
||||
<p className="mt-1 text-xs text-red-400">{t('inventory.bulk.invalidHex')}</p>
|
||||
<p className="mt-1 text-xs text-red-600 dark:text-red-400">{t('inventory.bulk.invalidHex')}</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -192,7 +192,7 @@ export function CalendarView({ archives, onArchiveClick, highlightedArchiveId }:
|
||||
<div className="text-xs text-bambu-gray">Prints this month</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-2xl font-bold text-green-400">
|
||||
<div className="text-2xl font-bold text-green-700 dark:text-green-400">
|
||||
{archives.filter(a => {
|
||||
const d = parseUTCDate(a.completed_at || a.created_at) || new Date();
|
||||
return d.getMonth() === currentMonth && d.getFullYear() === currentYear && a.status === 'completed';
|
||||
@@ -201,7 +201,7 @@ export function CalendarView({ archives, onArchiveClick, highlightedArchiveId }:
|
||||
<div className="text-xs text-bambu-gray">Successful</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-2xl font-bold text-red-400">
|
||||
<div className="text-2xl font-bold text-red-700 dark:text-red-400">
|
||||
{archives.filter(a => {
|
||||
const d = parseUTCDate(a.completed_at || a.created_at) || new Date();
|
||||
return d.getMonth() === currentMonth && d.getFullYear() === currentYear && a.status === 'failed';
|
||||
@@ -257,7 +257,7 @@ export function CalendarView({ archives, onArchiveClick, highlightedArchiveId }:
|
||||
{archive.print_name || archive.filename}
|
||||
</p>
|
||||
<div className="flex items-center gap-2 text-xs">
|
||||
<span className={archive.status === 'failed' ? 'text-red-400' : 'text-green-400'}>
|
||||
<span className={archive.status === 'failed' ? 'text-red-700 dark:text-red-400' : 'text-green-700 dark:text-green-400'}>
|
||||
{archive.status === 'failed' ? 'Failed' : 'Completed'}
|
||||
</span>
|
||||
{archive.filament_color && (
|
||||
|
||||
@@ -12,7 +12,7 @@ interface CameraDiagnoseModalProps {
|
||||
|
||||
function StageIcon({ status }: { status: CameraDiagnoseStage['status'] }) {
|
||||
if (status === 'ok') return <CheckCircle2 className="w-5 h-5 text-bambu-green flex-shrink-0" />;
|
||||
if (status === 'failed') return <XCircle className="w-5 h-5 text-red-400 flex-shrink-0" />;
|
||||
if (status === 'failed') return <XCircle className="w-5 h-5 text-red-600 dark:text-red-400 flex-shrink-0" />;
|
||||
return <MinusCircle className="w-5 h-5 text-bambu-gray flex-shrink-0" />;
|
||||
}
|
||||
|
||||
@@ -77,7 +77,7 @@ export function CameraDiagnoseModal({ printerId, printerName, onClose }: CameraD
|
||||
)}
|
||||
|
||||
{diagnose.isError && (
|
||||
<div className="rounded-lg bg-red-500/10 border border-red-500/30 px-4 py-3 text-sm text-red-300">
|
||||
<div className="rounded-lg bg-red-50 dark:bg-red-500/10 border border-red-300 dark:border-red-500/30 px-4 py-3 text-sm text-red-700 dark:text-red-300">
|
||||
{t('camera.diagnose.runFailed', { error: (diagnose.error as Error).message })}
|
||||
</div>
|
||||
)}
|
||||
@@ -112,7 +112,7 @@ export function CameraDiagnoseModal({ printerId, printerName, onClose }: CameraD
|
||||
className={
|
||||
result.overall_status === 'ok'
|
||||
? 'rounded-lg bg-bambu-green/10 border border-bambu-green/30 px-4 py-3 text-sm text-bambu-green'
|
||||
: 'rounded-lg bg-red-500/10 border border-red-500/30 px-4 py-3 text-sm text-red-300'
|
||||
: 'rounded-lg bg-red-50 dark:bg-red-500/10 border border-red-300 dark:border-red-500/30 px-4 py-3 text-sm text-red-700 dark:text-red-300'
|
||||
}
|
||||
>
|
||||
{t(`camera.diagnose.summary.${result.summary_code}`, {
|
||||
|
||||
@@ -381,8 +381,8 @@ export function ColorCatalogSettings() {
|
||||
</button>
|
||||
</div>
|
||||
{selectedIds.size > 0 && (
|
||||
<div className="flex items-center gap-2 mt-2 px-3 py-2 bg-red-500/10 border border-red-500/30 rounded-lg">
|
||||
<span className="text-sm text-red-400">
|
||||
<div className="flex items-center gap-2 mt-2 px-3 py-2 bg-red-50 dark:bg-red-500/10 border border-red-300 dark:border-red-500/30 rounded-lg">
|
||||
<span className="text-sm text-red-700 dark:text-red-400">
|
||||
{t('settings.colorCatalog.selectedCount', { count: selectedIds.size })}
|
||||
</span>
|
||||
<button
|
||||
|
||||
@@ -18,9 +18,9 @@ import type { PrintQueueItem, Permission } from '../api/client';
|
||||
import { Button } from './Button';
|
||||
|
||||
const STATUS_CONFIG = {
|
||||
completed: { icon: CheckCircle, color: 'text-emerald-400', border: 'border-l-emerald-500' },
|
||||
failed: { icon: XCircle, color: 'text-red-400', border: 'border-l-red-500' },
|
||||
skipped: { icon: SkipForward, color: 'text-orange-400', border: 'border-l-gray-500' },
|
||||
completed: { icon: CheckCircle, color: 'text-emerald-600 dark:text-emerald-400', border: 'border-l-emerald-500' },
|
||||
failed: { icon: XCircle, color: 'text-red-600 dark:text-red-400', border: 'border-l-red-500' },
|
||||
skipped: { icon: SkipForward, color: 'text-orange-600 dark:text-orange-400', border: 'border-l-gray-500' },
|
||||
cancelled: { icon: Ban, color: 'text-gray-400', border: 'border-l-gray-500' },
|
||||
} as const;
|
||||
|
||||
@@ -182,7 +182,7 @@ export function CompactHistoryRow({
|
||||
|
||||
{/* Error message — only rendered on failed/skipped rows. */}
|
||||
{showErrorMessage && (
|
||||
<div className="mt-1 ml-[3.25rem] flex items-start gap-1 text-xs text-red-400">
|
||||
<div className="mt-1 ml-[3.25rem] flex items-start gap-1 text-xs text-red-700 dark:text-red-400">
|
||||
<AlertCircle className="w-3 h-3 mt-0.5 shrink-0" />
|
||||
<span className="break-words">{item.error_message}</span>
|
||||
</div>
|
||||
|
||||
@@ -48,7 +48,7 @@ export function CompareArchivesModal({ archiveIds, onClose }: CompareArchivesMod
|
||||
<Loader2 className="w-8 h-8 text-bambu-green animate-spin" />
|
||||
</div>
|
||||
) : error ? (
|
||||
<div className="text-center py-12 text-red-400">
|
||||
<div className="text-center py-12 text-red-700 dark:text-red-400">
|
||||
<AlertTriangle className="w-12 h-12 mx-auto mb-4 opacity-50" />
|
||||
<p>Failed to load comparison</p>
|
||||
<p className="text-sm text-bambu-gray mt-2">
|
||||
@@ -109,9 +109,9 @@ function ComparisonContent({ comparison }: { comparison: ArchiveComparison }) {
|
||||
<td className="py-2 pr-4 text-sm">
|
||||
<div className="flex items-center gap-2">
|
||||
{field.has_difference && (
|
||||
<AlertTriangle className="w-3 h-3 text-yellow-400 flex-shrink-0" />
|
||||
<AlertTriangle className="w-3 h-3 text-yellow-600 dark:text-yellow-400 flex-shrink-0" />
|
||||
)}
|
||||
<span className={field.has_difference ? 'text-yellow-400' : 'text-bambu-gray'}>
|
||||
<span className={field.has_difference ? 'text-yellow-700 dark:text-yellow-400' : 'text-bambu-gray'}>
|
||||
{field.label}
|
||||
</span>
|
||||
</div>
|
||||
@@ -132,15 +132,15 @@ function ComparisonContent({ comparison }: { comparison: ArchiveComparison }) {
|
||||
|
||||
{/* Differences Summary */}
|
||||
{comparison.differences.length > 0 && (
|
||||
<div className="p-4 bg-yellow-500/10 border border-yellow-500/30 rounded-lg">
|
||||
<h4 className="text-sm font-medium text-yellow-400 mb-2 flex items-center gap-2">
|
||||
<div className="p-4 bg-yellow-50 border border-yellow-300 dark:bg-yellow-500/10 dark:border-yellow-500/30 rounded-lg">
|
||||
<h4 className="text-sm font-medium text-yellow-700 dark:text-yellow-400 mb-2 flex items-center gap-2">
|
||||
<AlertTriangle className="w-4 h-4" />
|
||||
{comparison.differences.length} Difference{comparison.differences.length > 1 ? 's' : ''} Found
|
||||
</h4>
|
||||
<ul className="text-sm text-white/80 space-y-1">
|
||||
{comparison.differences.slice(0, 5).map((diff) => (
|
||||
<li key={diff.field}>
|
||||
<span className="text-yellow-400">{diff.label}</span>: {diff.values.join(' vs ')} {diff.unit || ''}
|
||||
<span className="text-yellow-700 dark:text-yellow-400">{diff.label}</span>: {diff.values.join(' vs ')} {diff.unit || ''}
|
||||
</li>
|
||||
))}
|
||||
{comparison.differences.length > 5 && (
|
||||
@@ -163,7 +163,7 @@ function ComparisonContent({ comparison }: { comparison: ArchiveComparison }) {
|
||||
<span className="text-bambu-green">
|
||||
{comparison.success_correlation.successful_count} successful
|
||||
</span>
|
||||
<span className="text-red-400">
|
||||
<span className="text-red-700 dark:text-red-400">
|
||||
{comparison.success_correlation.failed_count} failed
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -1163,7 +1163,7 @@ export function ConfigureAmsSlotModal({
|
||||
{/* Left column: Filament preset list (takes full height) */}
|
||||
<div className="w-1/2 flex flex-col min-h-0">
|
||||
<label className="block text-sm text-bambu-gray mb-2">
|
||||
{t('configureAmsSlot.filamentProfile')} <span className="text-red-400">*</span>
|
||||
{t('configureAmsSlot.filamentProfile')} <span className="text-red-600 dark:text-red-400">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
@@ -1195,12 +1195,12 @@ export function ConfigureAmsSlotModal({
|
||||
<span className="text-white text-sm truncate group-hover:whitespace-normal group-hover:break-all" title={preset.name}>{preset.name}</span>
|
||||
<div className="flex items-center gap-1 flex-shrink-0">
|
||||
{preset.source === 'local' && (
|
||||
<span className="text-xs px-1.5 py-0.5 rounded bg-green-500/20 text-green-400">
|
||||
<span className="text-xs px-1.5 py-0.5 rounded bg-green-100 dark:bg-green-500/20 text-green-700 dark:text-green-400">
|
||||
{t('profiles.localProfiles.badge')}
|
||||
</span>
|
||||
)}
|
||||
{preset.source === 'orca_cloud' && (
|
||||
<span className="text-xs px-1.5 py-0.5 rounded bg-purple-500/20 text-purple-400">
|
||||
<span className="text-xs px-1.5 py-0.5 rounded bg-purple-100 dark:bg-purple-500/20 text-purple-700 dark:text-purple-400">
|
||||
{t('configureAmsSlot.orcaCloud')}
|
||||
</span>
|
||||
)}
|
||||
@@ -1210,7 +1210,7 @@ export function ConfigureAmsSlotModal({
|
||||
</span>
|
||||
)}
|
||||
{preset.source === 'builtin' && (
|
||||
<span className="text-xs px-1.5 py-0.5 rounded bg-amber-500/20 text-amber-400">
|
||||
<span className="text-xs px-1.5 py-0.5 rounded bg-amber-100 dark:bg-amber-500/20 text-amber-700 dark:text-amber-400">
|
||||
{t('configureAmsSlot.builtin')}
|
||||
</span>
|
||||
)}
|
||||
@@ -1258,7 +1258,7 @@ export function ConfigureAmsSlotModal({
|
||||
{t('configureAmsSlot.noMatchingKProfiles')}
|
||||
</p>
|
||||
) : (
|
||||
<span className="inline-block text-xs px-2 py-1 rounded bg-amber-500/20 text-amber-400 border border-amber-500/30">
|
||||
<span className="inline-block text-xs px-2 py-1 rounded bg-amber-100 dark:bg-amber-500/20 text-amber-700 dark:text-amber-400 border border-amber-300 dark:border-amber-500/30">
|
||||
{t('configureAmsSlot.selectFilamentFirst')}
|
||||
</span>
|
||||
)}
|
||||
@@ -1402,7 +1402,7 @@ export function ConfigureAmsSlotModal({
|
||||
{/* Filament Profile Select */}
|
||||
<div>
|
||||
<label className="block text-sm text-bambu-gray mb-2">
|
||||
{t('configureAmsSlot.filamentProfile')} <span className="text-red-400">*</span>
|
||||
{t('configureAmsSlot.filamentProfile')} <span className="text-red-600 dark:text-red-400">*</span>
|
||||
</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
@@ -1435,12 +1435,12 @@ export function ConfigureAmsSlotModal({
|
||||
<span className="text-white text-sm truncate group-hover:whitespace-normal group-hover:break-all" title={preset.name}>{preset.name}</span>
|
||||
<div className="flex items-center gap-1 flex-shrink-0">
|
||||
{preset.source === 'local' && (
|
||||
<span className="text-xs px-1.5 py-0.5 rounded bg-green-500/20 text-green-400">
|
||||
<span className="text-xs px-1.5 py-0.5 rounded bg-green-100 dark:bg-green-500/20 text-green-700 dark:text-green-400">
|
||||
{t('profiles.localProfiles.badge')}
|
||||
</span>
|
||||
)}
|
||||
{preset.source === 'orca_cloud' && (
|
||||
<span className="text-xs px-1.5 py-0.5 rounded bg-purple-500/20 text-purple-400">
|
||||
<span className="text-xs px-1.5 py-0.5 rounded bg-purple-100 dark:bg-purple-500/20 text-purple-700 dark:text-purple-400">
|
||||
{t('configureAmsSlot.orcaCloud')}
|
||||
</span>
|
||||
)}
|
||||
@@ -1450,7 +1450,7 @@ export function ConfigureAmsSlotModal({
|
||||
</span>
|
||||
)}
|
||||
{preset.source === 'builtin' && (
|
||||
<span className="text-xs px-1.5 py-0.5 rounded bg-amber-500/20 text-amber-400">
|
||||
<span className="text-xs px-1.5 py-0.5 rounded bg-amber-100 dark:bg-amber-500/20 text-amber-700 dark:text-amber-400">
|
||||
{t('configureAmsSlot.builtin')}
|
||||
</span>
|
||||
)}
|
||||
@@ -1497,7 +1497,7 @@ export function ConfigureAmsSlotModal({
|
||||
{t('configureAmsSlot.noMatchingKProfiles')}
|
||||
</p>
|
||||
) : (
|
||||
<span className="inline-block text-xs px-2 py-1 rounded bg-amber-500/20 text-amber-400 border border-amber-500/30">
|
||||
<span className="inline-block text-xs px-2 py-1 rounded bg-amber-100 dark:bg-amber-500/20 text-amber-700 dark:text-amber-400 border border-amber-300 dark:border-amber-500/30">
|
||||
{t('configureAmsSlot.selectFilamentFirst')}
|
||||
</span>
|
||||
)}
|
||||
@@ -1653,7 +1653,7 @@ export function ConfigureAmsSlotModal({
|
||||
variant="secondary"
|
||||
onClick={() => resetMutation.mutate()}
|
||||
disabled={resetMutation.isPending || configureMutation.isPending}
|
||||
className="text-red-400 hover:text-red-300 hover:bg-red-500/10"
|
||||
className="text-red-600 dark:text-red-400 hover:text-red-700 dark:hover:text-red-300 hover:bg-red-50 dark:hover:bg-red-500/10"
|
||||
>
|
||||
{resetMutation.isPending ? (
|
||||
<>
|
||||
@@ -1693,7 +1693,7 @@ export function ConfigureAmsSlotModal({
|
||||
|
||||
{/* Error */}
|
||||
{(configureMutation.isError || resetMutation.isError) && (
|
||||
<div className="mx-4 mb-4 p-2 bg-red-500/20 border border-red-500/50 rounded text-sm text-red-400">
|
||||
<div className="mx-4 mb-4 p-2 bg-red-100 dark:bg-red-500/20 border border-red-300 dark:border-red-500/50 rounded text-sm text-red-700 dark:text-red-400">
|
||||
{(configureMutation.error as Error)?.message || (resetMutation.error as Error)?.message}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -63,11 +63,11 @@ export function ConfirmModal({
|
||||
|
||||
const variantStyles = {
|
||||
danger: {
|
||||
icon: 'text-red-400',
|
||||
icon: 'text-red-600 dark:text-red-400',
|
||||
button: 'bg-red-500 hover:bg-red-600',
|
||||
},
|
||||
warning: {
|
||||
icon: 'text-yellow-400',
|
||||
icon: 'text-yellow-600 dark:text-yellow-400',
|
||||
button: 'bg-yellow-500 hover:bg-yellow-600 text-black',
|
||||
},
|
||||
default: {
|
||||
|
||||
@@ -19,8 +19,8 @@ import {
|
||||
|
||||
function StatusIcon({ status }: { status: DiagnosticStatus }) {
|
||||
if (status === 'pass') return <CheckCircle2 className="w-5 h-5 text-bambu-green flex-shrink-0" />;
|
||||
if (status === 'fail') return <XCircle className="w-5 h-5 text-red-400 flex-shrink-0" />;
|
||||
if (status === 'warn') return <AlertTriangle className="w-5 h-5 text-amber-400 flex-shrink-0" />;
|
||||
if (status === 'fail') return <XCircle className="w-5 h-5 text-red-600 dark:text-red-400 flex-shrink-0" />;
|
||||
if (status === 'warn') return <AlertTriangle className="w-5 h-5 text-amber-600 dark:text-amber-400 flex-shrink-0" />;
|
||||
return <MinusCircle className="w-5 h-5 text-bambu-gray flex-shrink-0" />;
|
||||
}
|
||||
|
||||
@@ -36,8 +36,8 @@ export function DiagnosticChecklist({ result }: { result: PrinterDiagnosticResul
|
||||
result.overall === 'ok'
|
||||
? 'bg-bambu-green/10 border-bambu-green/30 text-bambu-green'
|
||||
: result.overall === 'warnings'
|
||||
? 'bg-amber-500/10 border-amber-500/30 text-amber-300'
|
||||
: 'bg-red-500/10 border-red-500/30 text-red-300';
|
||||
? 'bg-amber-50 dark:bg-amber-500/10 border-amber-300 dark:border-amber-500/30 text-amber-700 dark:text-amber-300'
|
||||
: 'bg-red-50 dark:bg-red-500/10 border-red-300 dark:border-red-500/30 text-red-700 dark:text-red-300';
|
||||
|
||||
const renderCheck = (check: DiagnosticCheck) => {
|
||||
const params =
|
||||
@@ -189,7 +189,7 @@ export function ConnectionDiagnosticModal(props: ConnectionDiagnosticModalProps)
|
||||
)}
|
||||
|
||||
{diagnose.isError && (
|
||||
<div className="rounded-lg bg-red-500/10 border border-red-500/30 px-4 py-3 text-sm text-red-300">
|
||||
<div className="rounded-lg bg-red-50 dark:bg-red-500/10 border border-red-300 dark:border-red-500/30 px-4 py-3 text-sm text-red-700 dark:text-red-300">
|
||||
{t('diagnostic.runFailed', { error: (diagnose.error as Error).message })}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -99,7 +99,7 @@ function SubmenuPanel({
|
||||
subItem.disabled
|
||||
? 'text-bambu-gray cursor-not-allowed'
|
||||
: subItem.danger
|
||||
? 'text-red-400 hover:bg-red-400/10'
|
||||
? 'text-red-700 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-400/10'
|
||||
: 'text-white hover:bg-bambu-dark-tertiary'
|
||||
}`}
|
||||
>
|
||||
@@ -269,7 +269,7 @@ export function ContextMenu({ x, y, items, onClose }: ContextMenuProps) {
|
||||
item.disabled
|
||||
? 'text-bambu-gray cursor-not-allowed'
|
||||
: item.danger
|
||||
? 'text-red-400 hover:bg-red-400/10'
|
||||
? 'text-red-700 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-400/10'
|
||||
: 'text-white hover:bg-bambu-dark-tertiary'
|
||||
} ${hasSubmenu && activeSubmenu === index ? 'bg-bambu-dark-tertiary' : ''}`}
|
||||
>
|
||||
|
||||
@@ -137,7 +137,7 @@ export function CreateUserAdvancedAuthModal({
|
||||
{/* Username Field */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-white mb-2">
|
||||
{t('users.form.username')} <span className="text-red-400">*</span>
|
||||
{t('users.form.username')} <span className="text-red-600 dark:text-red-400">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
@@ -153,7 +153,7 @@ export function CreateUserAdvancedAuthModal({
|
||||
{/* Email Field */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-white mb-2">
|
||||
{t('users.form.email') || 'Email'} <span className="text-red-400">*</span>
|
||||
{t('users.form.email') || 'Email'} <span className="text-red-600 dark:text-red-400">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
@@ -191,7 +191,7 @@ export function CreateUserAdvancedAuthModal({
|
||||
/>
|
||||
<span className="text-sm text-white">{group.name}</span>
|
||||
{group.is_system && (
|
||||
<span className="text-xs text-yellow-400">({t('users.system')})</span>
|
||||
<span className="text-xs text-yellow-700 dark:text-yellow-400">({t('users.system')})</span>
|
||||
)}
|
||||
</label>
|
||||
))}
|
||||
|
||||
@@ -207,14 +207,14 @@ export function EmailSettings() {
|
||||
<CardContent>
|
||||
<div className="space-y-3">
|
||||
{advancedEnabled ? (
|
||||
<div className="bg-green-500/10 border border-green-500/30 rounded-lg p-4">
|
||||
<div className="bg-green-50 dark:bg-green-500/10 border border-green-300 dark:border-green-500/30 rounded-lg p-4">
|
||||
<div className="flex items-start gap-3">
|
||||
<CheckCircle className="w-5 h-5 text-green-400 mt-0.5 flex-shrink-0" />
|
||||
<CheckCircle className="w-5 h-5 text-green-600 dark:text-green-400 mt-0.5 flex-shrink-0" />
|
||||
<div className="space-y-2">
|
||||
<p className="text-white font-medium">
|
||||
{t('settings.email.advancedAuthEnabled') || 'Advanced Authentication is enabled'}
|
||||
</p>
|
||||
<ul className="text-sm text-green-300 space-y-1 list-disc list-inside">
|
||||
<ul className="text-sm text-green-700 dark:text-green-300 space-y-1 list-disc list-inside">
|
||||
<li>{t('settings.email.feature1') || 'Passwords are auto-generated and emailed to new users'}</li>
|
||||
<li>{t('settings.email.feature2') || 'Users can login with username or email'}</li>
|
||||
<li>{t('settings.email.feature3') || 'Forgot password feature is available'}</li>
|
||||
@@ -224,14 +224,14 @@ export function EmailSettings() {
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="bg-yellow-500/10 border border-yellow-500/30 rounded-lg p-4">
|
||||
<div className="bg-yellow-50 dark:bg-yellow-500/10 border border-yellow-300 dark:border-yellow-500/30 rounded-lg p-4">
|
||||
<div className="flex items-start gap-3">
|
||||
<AlertTriangle className="w-5 h-5 text-yellow-400 mt-0.5 flex-shrink-0" />
|
||||
<AlertTriangle className="w-5 h-5 text-yellow-600 dark:text-yellow-400 mt-0.5 flex-shrink-0" />
|
||||
<div className="space-y-2">
|
||||
<p className="text-white font-medium">
|
||||
{t('settings.email.advancedAuthDisabled') || 'Advanced Authentication is disabled'}
|
||||
</p>
|
||||
<p className="text-sm text-yellow-300">
|
||||
<p className="text-sm text-yellow-700 dark:text-yellow-300">
|
||||
{t('settings.email.advancedAuthDisabledDesc') || 'Enable advanced authentication to activate email-based features for user management.'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -644,7 +644,7 @@ export function EmbeddedCameraViewer({ printerId, printerName, viewerIndex = 0,
|
||||
className="p-1 hover:bg-red-500/20 rounded"
|
||||
title="Close"
|
||||
>
|
||||
<X className="w-3.5 h-3.5 text-bambu-gray hover:text-red-400" />
|
||||
<X className="w-3.5 h-3.5 text-bambu-gray hover:text-red-600 dark:hover:text-red-400" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -337,7 +337,7 @@ export function ExternalLinksSettings() {
|
||||
<button
|
||||
onClick={() => handleDelete(link)}
|
||||
disabled={deleteMutation.isPending}
|
||||
className="p-2 rounded-lg hover:bg-red-500/20 text-bambu-gray hover:text-red-400 transition-colors disabled:opacity-50"
|
||||
className="p-2 rounded-lg hover:bg-red-100 dark:hover:bg-red-500/20 text-bambu-gray hover:text-red-900 dark:hover:text-red-400 transition-colors disabled:opacity-50"
|
||||
title={t('externalLinks.deleteLink')}
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
|
||||
@@ -163,7 +163,7 @@ export function FailureDetectionSettings() {
|
||||
{testResult && (
|
||||
<div
|
||||
className={`flex items-start gap-2 mt-2 text-sm ${
|
||||
testResult.ok ? 'text-green-400' : 'text-red-400'
|
||||
testResult.ok ? 'text-green-700 dark:text-green-400' : 'text-red-700 dark:text-red-400'
|
||||
}`}
|
||||
>
|
||||
{testResult.ok ? <Check className="w-4 h-4 mt-0.5" /> : <X className="w-4 h-4 mt-0.5" />}
|
||||
@@ -222,7 +222,7 @@ export function FailureDetectionSettings() {
|
||||
</div>
|
||||
|
||||
{status && !status.external_url_configured && enabled && (
|
||||
<div className="flex items-start gap-2 p-3 bg-amber-900/30 border border-amber-700 rounded text-sm text-amber-200">
|
||||
<div className="flex items-start gap-2 p-3 bg-amber-50 dark:bg-amber-900/30 border border-amber-300 dark:border-amber-700 rounded text-sm text-amber-800 dark:text-amber-200">
|
||||
<AlertTriangle className="w-4 h-4 mt-0.5 flex-shrink-0" />
|
||||
<div>
|
||||
<div className="font-medium">{t('failureDetection.externalUrlMissing')}</div>
|
||||
@@ -282,7 +282,7 @@ export function FailureDetectionSettings() {
|
||||
<div className="space-y-3 text-sm">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-bambu-gray">{t('failureDetection.serviceRunning')}</span>
|
||||
<span className={status.is_running ? 'text-green-400' : 'text-red-400'}>
|
||||
<span className={status.is_running ? 'text-green-700 dark:text-green-400' : 'text-red-700 dark:text-red-400'}>
|
||||
{status.is_running ? t('common.yes') : t('common.no')}
|
||||
</span>
|
||||
</div>
|
||||
@@ -293,7 +293,7 @@ export function FailureDetectionSettings() {
|
||||
</span>
|
||||
</div>
|
||||
{status.last_error && (
|
||||
<div className="flex items-start gap-2 text-red-400">
|
||||
<div className="flex items-start gap-2 text-red-700 dark:text-red-400">
|
||||
<X className="w-4 h-4 mt-0.5 flex-shrink-0" />
|
||||
<span className="break-words">{status.last_error}</span>
|
||||
</div>
|
||||
@@ -308,10 +308,10 @@ export function FailureDetectionSettings() {
|
||||
const printer = printers?.find((p) => String(p.id) === pid);
|
||||
const colorClass =
|
||||
info.class === 'failure'
|
||||
? 'text-red-400'
|
||||
? 'text-red-700 dark:text-red-400'
|
||||
: info.class === 'warning'
|
||||
? 'text-amber-400'
|
||||
: 'text-green-400';
|
||||
? 'text-amber-700 dark:text-amber-400'
|
||||
: 'text-green-700 dark:text-green-400';
|
||||
return (
|
||||
<div key={pid} className="flex justify-between">
|
||||
<span className="text-white">{printer?.name ?? `Printer ${pid}`}</span>
|
||||
@@ -350,9 +350,9 @@ export function FailureDetectionSettings() {
|
||||
const printer = printers?.find((p) => p.id === ev.printer_id);
|
||||
const colorClass =
|
||||
ev.class === 'failure'
|
||||
? 'text-red-400'
|
||||
? 'text-red-700 dark:text-red-400'
|
||||
: ev.class === 'warning'
|
||||
? 'text-amber-400'
|
||||
? 'text-amber-700 dark:text-amber-400'
|
||||
: 'text-bambu-gray';
|
||||
return (
|
||||
<div key={idx} className="flex justify-between gap-2 py-1 border-b border-gray-800">
|
||||
|
||||
@@ -395,7 +395,7 @@ export function FilamentHoverCard({ data, children, disabled, className = '', sp
|
||||
e.stopPropagation();
|
||||
inventory.onUnassignSpool?.();
|
||||
}}
|
||||
className="w-full flex items-center justify-center gap-1.5 px-2 py-1.5 text-xs font-medium rounded transition-colors bg-red-500/20 hover:bg-red-500/30 text-red-400"
|
||||
className="w-full flex items-center justify-center gap-1.5 px-2 py-1.5 text-xs font-medium rounded transition-colors bg-red-100 dark:bg-red-500/20 hover:bg-red-200 dark:hover:bg-red-500/30 text-red-700 dark:text-red-400"
|
||||
>
|
||||
<Unlink className="w-3.5 h-3.5" />
|
||||
{t('inventory.unassignSpool')}
|
||||
@@ -488,7 +488,7 @@ export function FilamentHoverCard({ data, children, disabled, className = '', sp
|
||||
spoolman?.onUnlinkSpool?.();
|
||||
setShowUnlinkConfirm(false);
|
||||
}}
|
||||
className="flex-1 px-3 py-2 text-sm font-medium rounded transition-colors bg-red-500/20 hover:bg-red-500/30 text-red-400"
|
||||
className="flex-1 px-3 py-2 text-sm font-medium rounded transition-colors bg-red-100 dark:bg-red-500/20 hover:bg-red-200 dark:hover:bg-red-500/30 text-red-700 dark:text-red-400"
|
||||
>
|
||||
{t('inventory.unassignSpool')}
|
||||
</button>
|
||||
|
||||
@@ -702,7 +702,7 @@ export function FileManagerModal({ printerId, printerName, onClose }: FileManage
|
||||
variant="secondary"
|
||||
disabled={selectedFiles.size === 0 || deleteMutation.isPending}
|
||||
onClick={handleDelete}
|
||||
className="text-red-400 hover:text-red-300"
|
||||
className="text-red-700 hover:text-red-800 dark:text-red-400 dark:hover:text-red-300"
|
||||
>
|
||||
{deleteMutation.isPending ? (
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
|
||||
@@ -215,12 +215,12 @@ export function FileUploadModal({ folderId, onClose, onUploadComplete, onFileUpl
|
||||
|
||||
{/* ZIP Options */}
|
||||
{hasZipFiles && (
|
||||
<div className="p-3 bg-blue-500/10 border border-blue-500/30 rounded-lg">
|
||||
<div className="p-3 bg-blue-50 dark:bg-blue-500/10 border border-blue-300 dark:border-blue-500/30 rounded-lg">
|
||||
<div className="flex items-start gap-3">
|
||||
<ArchiveIcon className="w-5 h-5 text-blue-400 mt-0.5 flex-shrink-0" />
|
||||
<ArchiveIcon className="w-5 h-5 text-blue-600 dark:text-blue-400 mt-0.5 flex-shrink-0" />
|
||||
<div className="flex-1">
|
||||
<p className="text-sm text-blue-300 font-medium">{t('fileManager.zipFilesDetected')}</p>
|
||||
<p className="text-xs text-blue-300/70 mt-1">
|
||||
<p className="text-sm text-blue-700 dark:text-blue-300 font-medium">{t('fileManager.zipFilesDetected')}</p>
|
||||
<p className="text-xs text-blue-700/80 dark:text-blue-300/70 mt-1">
|
||||
{t('fileManager.zipExtractOptions')}
|
||||
</p>
|
||||
<label className="flex items-center gap-2 mt-2 cursor-pointer">
|
||||
@@ -248,12 +248,12 @@ export function FileUploadModal({ folderId, onClose, onUploadComplete, onFileUpl
|
||||
|
||||
{/* 3MF File Info */}
|
||||
{has3mfFiles && (
|
||||
<div className="p-3 bg-purple-500/10 border border-purple-500/30 rounded-lg">
|
||||
<div className="p-3 bg-purple-50 dark:bg-purple-500/10 border border-purple-300 dark:border-purple-500/30 rounded-lg">
|
||||
<div className="flex items-start gap-3">
|
||||
<Printer className="w-5 h-5 text-purple-400 mt-0.5 flex-shrink-0" />
|
||||
<Printer className="w-5 h-5 text-purple-600 dark:text-purple-400 mt-0.5 flex-shrink-0" />
|
||||
<div className="flex-1">
|
||||
<p className="text-sm text-purple-300 font-medium">{t('fileManager.threemfDetected')}</p>
|
||||
<p className="text-xs text-purple-300/70 mt-1">
|
||||
<p className="text-sm text-purple-700 dark:text-purple-300 font-medium">{t('fileManager.threemfDetected')}</p>
|
||||
<p className="text-xs text-purple-700/80 dark:text-purple-300/70 mt-1">
|
||||
{t('fileManager.threemfExtractionInfo')}
|
||||
</p>
|
||||
</div>
|
||||
@@ -296,7 +296,7 @@ export function FileUploadModal({ folderId, onClose, onUploadComplete, onFileUpl
|
||||
className="flex items-center gap-3 p-2 bg-bambu-dark rounded-lg"
|
||||
>
|
||||
{uploadFile.isZip ? (
|
||||
<ArchiveIcon className="w-4 h-4 text-blue-400 flex-shrink-0" />
|
||||
<ArchiveIcon className="w-4 h-4 text-blue-600 dark:text-blue-400 flex-shrink-0" />
|
||||
) : (
|
||||
<File className="w-4 h-4 text-bambu-gray flex-shrink-0" />
|
||||
)}
|
||||
@@ -305,10 +305,10 @@ export function FileUploadModal({ folderId, onClose, onUploadComplete, onFileUpl
|
||||
<p className="text-xs text-bambu-gray">
|
||||
{(uploadFile.file.size / 1024 / 1024).toFixed(2)} MB
|
||||
{uploadFile.isZip && uploadFile.status === 'pending' && (
|
||||
<span className="text-blue-400 ml-2">• {t('fileManager.willBeExtracted')}</span>
|
||||
<span className="text-blue-700 dark:text-blue-400 ml-2">• {t('fileManager.willBeExtracted')}</span>
|
||||
)}
|
||||
{uploadFile.extractedCount !== undefined && (
|
||||
<span className="text-green-400 ml-2">• {t('fileManager.filesExtracted', { count: uploadFile.extractedCount })}</span>
|
||||
<span className="text-green-700 dark:text-green-400 ml-2">• {t('fileManager.filesExtracted', { count: uploadFile.extractedCount })}</span>
|
||||
)}
|
||||
</p>
|
||||
{/* #1401: errors render inline rather than as a hover-only
|
||||
@@ -316,7 +316,7 @@ export function FileUploadModal({ folderId, onClose, onUploadComplete, onFileUpl
|
||||
actual fix (re-export as .gcode.3mf) — useless if the
|
||||
user can't read them. */}
|
||||
{uploadFile.status === 'error' && uploadFile.error && (
|
||||
<p className="text-xs text-red-400 mt-1 break-words">{uploadFile.error}</p>
|
||||
<p className="text-xs text-red-700 dark:text-red-400 mt-1 break-words">{uploadFile.error}</p>
|
||||
)}
|
||||
</div>
|
||||
{uploadFile.status === 'pending' && (
|
||||
@@ -345,10 +345,10 @@ export function FileUploadModal({ folderId, onClose, onUploadComplete, onFileUpl
|
||||
|
||||
{/* Compatibility Error */}
|
||||
{uploadError && (
|
||||
<div className="p-3 bg-red-500/10 border border-red-500/30 rounded-lg">
|
||||
<div className="p-3 bg-red-50 dark:bg-red-500/10 border border-red-300 dark:border-red-500/30 rounded-lg">
|
||||
<div className="flex items-start gap-3">
|
||||
<XCircle className="w-5 h-5 text-red-400 mt-0.5 flex-shrink-0" />
|
||||
<p className="text-sm text-red-300">{uploadError}</p>
|
||||
<XCircle className="w-5 h-5 text-red-600 dark:text-red-400 mt-0.5 flex-shrink-0" />
|
||||
<p className="text-sm text-red-700 dark:text-red-300">{uploadError}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -42,7 +42,7 @@ export function FolderReadmePanel({ folderId }: FolderReadmePanelProps) {
|
||||
{data.filename}
|
||||
</span>
|
||||
{data.truncated && (
|
||||
<span className="text-xs px-1.5 py-0.5 rounded bg-amber-500/20 text-amber-400 flex-shrink-0">
|
||||
<span className="text-xs px-1.5 py-0.5 rounded bg-amber-100 dark:bg-amber-500/20 text-amber-700 dark:text-amber-400 flex-shrink-0">
|
||||
{t('fileManager.readme.truncated')}
|
||||
</span>
|
||||
)}
|
||||
|
||||
@@ -387,8 +387,8 @@ export function ForecastPanel({ spools }: { spools: InventorySpool[] }) {
|
||||
onClick={() => setAlertsOpen((o) => !o)}
|
||||
className={`flex items-center gap-2 px-3 py-1.5 rounded-lg border text-sm font-medium transition-colors ${
|
||||
alerts.some((f) => f.stockBreakAlert)
|
||||
? 'bg-red-500/15 border-red-500/30 text-red-300 hover:bg-red-500/25'
|
||||
: 'bg-yellow-500/15 border-yellow-500/30 text-yellow-300 hover:bg-yellow-500/25'
|
||||
? 'bg-red-100 dark:bg-red-500/15 border-red-300 dark:border-red-500/30 text-red-700 dark:text-red-300 hover:bg-red-200 dark:hover:bg-red-500/25'
|
||||
: 'bg-yellow-100 dark:bg-yellow-500/15 border-yellow-300 dark:border-yellow-500/30 text-yellow-700 dark:text-yellow-300 hover:bg-yellow-200 dark:hover:bg-yellow-500/25'
|
||||
}`}
|
||||
>
|
||||
<AlertTriangle className="w-4 h-4" />
|
||||
@@ -617,7 +617,7 @@ function AlertBanner({ forecast: f, onCart }: { forecast: SkuForecast; onCart: (
|
||||
|
||||
return (
|
||||
<div className={`flex items-center gap-3 px-4 py-3 rounded-lg border text-sm ${
|
||||
isBreak ? 'bg-red-500/10 border-red-500/30 text-red-300' : 'bg-yellow-500/10 border-yellow-500/30 text-yellow-300'
|
||||
isBreak ? 'bg-red-50 dark:bg-red-500/10 border-red-300 dark:border-red-500/30 text-red-700 dark:text-red-300' : 'bg-yellow-50 dark:bg-yellow-500/10 border-yellow-300 dark:border-yellow-500/30 text-yellow-700 dark:text-yellow-300'
|
||||
}`}>
|
||||
<AlertTriangle className="w-4 h-4 flex-shrink-0" />
|
||||
<div className="flex-1 min-w-0">
|
||||
@@ -876,10 +876,10 @@ function ForecastRow({
|
||||
|
||||
const daysColor = snoozed ? 'text-bambu-gray'
|
||||
: f.daysRemaining === null ? 'text-bambu-gray'
|
||||
: f.stockBreakAlert ? 'text-red-400'
|
||||
: f.reorderAlert ? 'text-yellow-400'
|
||||
: f.daysRemaining < 30 ? 'text-yellow-400'
|
||||
: 'text-green-400';
|
||||
: f.stockBreakAlert ? 'text-red-700 dark:text-red-400'
|
||||
: f.reorderAlert ? 'text-yellow-700 dark:text-yellow-400'
|
||||
: f.daysRemaining < 30 ? 'text-yellow-700 dark:text-yellow-400'
|
||||
: 'text-green-700 dark:text-green-400';
|
||||
|
||||
function upsert(lead: number, marginVal: number, marginUnitArg: 'days' | 'g', alertsSnoozed = snoozed) {
|
||||
upsertMutation.mutate({ material: f.group.material, subtype: f.group.subtype, brand: f.group.brand, color_name: f.group.colorName, lead_time_days: lead, safety_margin_value: marginVal, safety_margin_unit: marginUnitArg, alerts_snoozed: alertsSnoozed });
|
||||
@@ -893,7 +893,7 @@ function ForecastRow({
|
||||
const tierBadge = f.rateTier === 'history'
|
||||
? <span className="inline-flex items-center gap-1 text-xs px-1.5 py-0.5 rounded bg-bambu-green/15 text-bambu-green"><span className="w-1.5 h-1.5 rounded-full bg-bambu-green" />{t('forecast.trend')}</span>
|
||||
: f.rateTier === 'delta'
|
||||
? <span className="inline-flex items-center gap-1 text-xs px-1.5 py-0.5 rounded bg-blue-400/15 text-blue-400"><span className="w-1.5 h-1.5 rounded-full bg-blue-400" />{t('forecast.estimated')}</span>
|
||||
? <span className="inline-flex items-center gap-1 text-xs px-1.5 py-0.5 rounded bg-blue-100 dark:bg-blue-400/15 text-blue-700 dark:text-blue-400"><span className="w-1.5 h-1.5 rounded-full bg-blue-400" />{t('forecast.estimated')}</span>
|
||||
: <span className="inline-flex items-center gap-1 text-xs px-1.5 py-0.5 rounded bg-bambu-dark-tertiary text-bambu-gray/60"><span className="w-1.5 h-1.5 rounded-full bg-bambu-gray/40" />{t('forecast.noData')}</span>;
|
||||
|
||||
const rowAlertBorder = snoozed ? '' : f.stockBreakAlert ? 'bg-red-500/5' : f.reorderAlert ? 'bg-yellow-500/5' : '';
|
||||
@@ -959,7 +959,7 @@ function ForecastRow({
|
||||
|
||||
{/* Reorder by */}
|
||||
<td className="px-4 py-3">
|
||||
<span className={`text-sm ${!snoozed && f.reorderAlert ? 'text-yellow-400' : 'text-bambu-gray'}`}>
|
||||
<span className={`text-sm ${!snoozed && f.reorderAlert ? 'text-yellow-700 dark:text-yellow-400' : 'text-bambu-gray'}`}>
|
||||
{f.reorderTriggerDate ? formatDate(f.reorderTriggerDate) : '—'}
|
||||
</span>
|
||||
</td>
|
||||
@@ -977,16 +977,16 @@ function ForecastRow({
|
||||
</button>
|
||||
)}
|
||||
{!snoozed && (f.stockBreakAlert ? (
|
||||
<AlertTriangle className="w-4 h-4 text-red-400" aria-label={t('forecast.stockBreakRisk')} />
|
||||
<AlertTriangle className="w-4 h-4 text-red-600 dark:text-red-400" aria-label={t('forecast.stockBreakRisk')} />
|
||||
) : f.reorderAlert ? (
|
||||
<AlertTriangle className="w-4 h-4 text-yellow-400" aria-label={t('forecast.reorderNow')} />
|
||||
<AlertTriangle className="w-4 h-4 text-yellow-600 dark:text-yellow-400" aria-label={t('forecast.reorderNow')} />
|
||||
) : f.daysRemaining !== null ? (
|
||||
<Check className="w-4 h-4 text-bambu-green/50" />
|
||||
) : null)}
|
||||
{canWrite && (
|
||||
<button
|
||||
onClick={toggleSnooze}
|
||||
className={`p-1 rounded transition-colors ${snoozed ? 'text-amber-400/80 hover:text-amber-300' : 'text-slate-400 hover:text-white'}`}
|
||||
className={`p-1 rounded transition-colors ${snoozed ? 'text-amber-600/80 dark:text-amber-400/80 hover:text-amber-700 dark:hover:text-amber-300' : 'text-slate-400 hover:text-white'}`}
|
||||
title={t(snoozed ? 'forecast.alertsEnabled' : 'forecast.alertsSnoozed')}
|
||||
>
|
||||
<BellOff className="w-3.5 h-3.5" />
|
||||
@@ -1390,7 +1390,7 @@ function ShoppingListPanel({
|
||||
{t('forecast.downloadCsv')}
|
||||
</button>
|
||||
{canWrite && (
|
||||
<button onClick={onClear} className="text-xs text-red-400 hover:text-red-300 transition-colors px-2 py-1 rounded border border-red-500/20 hover:bg-red-500/10">
|
||||
<button onClick={onClear} className="text-xs text-red-700 dark:text-red-400 hover:text-red-800 dark:hover:text-red-300 transition-colors px-2 py-1 rounded border border-red-200 dark:border-red-500/20 hover:bg-red-50 dark:hover:bg-red-500/10">
|
||||
{t('forecast.clearAll')}
|
||||
</button>
|
||||
)}
|
||||
@@ -1444,7 +1444,7 @@ function ShoppingListPanel({
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-white">{lbl}</span>
|
||||
{hasBreak && !isPurchased && (
|
||||
<AlertTriangle className="w-3.5 h-3.5 text-red-400 flex-shrink-0" aria-label={t('forecast.stockBreakBefore')} />
|
||||
<AlertTriangle className="w-3.5 h-3.5 text-red-600 dark:text-red-400 flex-shrink-0" aria-label={t('forecast.stockBreakBefore')} />
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
@@ -1468,7 +1468,7 @@ function ShoppingListPanel({
|
||||
<td className="px-4 py-2.5">
|
||||
<span className={`inline-flex items-center px-2 py-0.5 rounded text-xs font-medium ${
|
||||
isReceived ? 'bg-bambu-green/20 text-bambu-green' :
|
||||
isPurchased ? 'bg-blue-400/20 text-blue-400' :
|
||||
isPurchased ? 'bg-blue-100 dark:bg-blue-400/20 text-blue-700 dark:text-blue-400' :
|
||||
'bg-bambu-dark-tertiary text-bambu-gray'
|
||||
}`}>
|
||||
{isReceived ? t('forecast.received') : isPurchased ? t('forecast.purchased') : t('forecast.pending')}
|
||||
@@ -1490,8 +1490,8 @@ function ShoppingListPanel({
|
||||
title={isPurchased ? t('forecast.resetToPending') : t('forecast.markPurchased')}
|
||||
className={`p-1.5 rounded transition-colors disabled:opacity-30 ${
|
||||
isPurchased
|
||||
? 'text-blue-400 hover:text-blue-300'
|
||||
: 'text-blue-400/50 hover:text-blue-400'
|
||||
? 'text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300'
|
||||
: 'text-blue-600/50 dark:text-blue-400/50 hover:text-blue-600 dark:hover:text-blue-400'
|
||||
}`}
|
||||
>
|
||||
{isPurchased ? <RotateCcw className="w-4 h-4" /> : <CreditCard className="w-4 h-4" />}
|
||||
@@ -1508,7 +1508,7 @@ function ShoppingListPanel({
|
||||
{/* Delete */}
|
||||
<button
|
||||
onClick={() => onRemove(item.id)}
|
||||
className="p-1 text-bambu-gray hover:text-red-400 transition-colors"
|
||||
className="p-1 text-bambu-gray hover:text-red-600 dark:hover:text-red-400 transition-colors"
|
||||
title={t('forecast.remove')}
|
||||
>
|
||||
<Trash2 className="w-3.5 h-3.5" />
|
||||
@@ -1608,14 +1608,14 @@ function CartLogisticsRow({
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
{hasBreak
|
||||
? <AlertTriangle className="w-4 h-4 text-red-400 flex-shrink-0" />
|
||||
? <AlertTriangle className="w-4 h-4 text-red-600 dark:text-red-400 flex-shrink-0" />
|
||||
: <Check className="w-4 h-4 text-bambu-green/60 flex-shrink-0" />
|
||||
}
|
||||
<span className="text-sm font-medium text-white truncate">{label}</span>
|
||||
<span className="text-xs text-bambu-gray flex-shrink-0">{t('forecast.spoolCount', { count: item.quantity_spools })} ordered</span>
|
||||
</div>
|
||||
{canWrite && (
|
||||
<button onClick={onRemove} className="p-1 text-bambu-gray hover:text-red-400 transition-colors flex-shrink-0">
|
||||
<button onClick={onRemove} className="p-1 text-bambu-gray hover:text-red-600 dark:hover:text-red-400 transition-colors flex-shrink-0">
|
||||
<Trash2 className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
)}
|
||||
@@ -1623,7 +1623,7 @@ function CartLogisticsRow({
|
||||
|
||||
{/* Break alert */}
|
||||
{hasBreak && (
|
||||
<div className="mb-3 px-3 py-2 rounded-lg bg-red-500/10 border border-red-500/20 text-xs text-red-300">
|
||||
<div className="mb-3 px-3 py-2 rounded-lg bg-red-50 dark:bg-red-500/10 border border-red-200 dark:border-red-500/20 text-xs text-red-700 dark:text-red-300">
|
||||
<span className="font-medium">{t('forecast.stockBreakIn', { days: stockBreaksAt })}</span>
|
||||
{' '}{t('forecast.stockRunsOutBefore', { lt: f!.effectiveLeadTimeDays })}
|
||||
{f!.dailyRateG !== null && (
|
||||
@@ -1657,9 +1657,9 @@ function CartLogisticsRow({
|
||||
<div className="text-xs text-bambu-gray mb-0.5">{t('forecast.safetyMarginLabel')}</div>
|
||||
<div className="text-sm font-semibold text-white">{Math.round(f.safetyStockG)}g</div>
|
||||
</div>
|
||||
<div className={`rounded-lg px-2.5 py-2 text-center ${hasBreak ? 'bg-red-500/15' : 'bg-bambu-dark-tertiary/40'}`}>
|
||||
<div className={`rounded-lg px-2.5 py-2 text-center ${hasBreak ? 'bg-red-100 dark:bg-red-500/15' : 'bg-bambu-dark-tertiary/40'}`}>
|
||||
<div className="text-xs text-bambu-gray mb-0.5">{t('forecast.daysLeft')}</div>
|
||||
<div className={`text-sm font-semibold ${hasBreak ? 'text-red-400' : 'text-green-400'}`}>
|
||||
<div className={`text-sm font-semibold ${hasBreak ? 'text-red-700 dark:text-red-400' : 'text-green-700 dark:text-green-400'}`}>
|
||||
{f.daysRemaining ?? '—'}d
|
||||
</div>
|
||||
</div>
|
||||
@@ -1771,7 +1771,7 @@ function CartLogisticsRow({
|
||||
<span className="flex items-center gap-1"><span className="inline-block w-4 border-t-2 border-yellow-400 border-dashed" /> {t('forecast.ropLabel')}</span>
|
||||
<span className="flex items-center gap-1"><span className="inline-block w-4 border-t border-bambu-gray border-dashed" /> {t('forecast.safetyStockLegend')}</span>
|
||||
<span className="flex items-center gap-1"><span className="inline-block w-4 border-t-2 border-blue-400 border-dashed" /> {t('forecast.stockArrivalLegend')}</span>
|
||||
{hasBreak && <span className="flex items-center gap-1 text-red-400"><span className="inline-block w-4 border-t-2 border-red-400" /> {t('forecast.stockoutLegend')}</span>}
|
||||
{hasBreak && <span className="flex items-center gap-1 text-red-700 dark:text-red-400"><span className="inline-block w-4 border-t-2 border-red-400" /> {t('forecast.stockoutLegend')}</span>}
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
@@ -1870,7 +1870,7 @@ function AddToCartModal({
|
||||
<span className="text-xs text-bambu-gray">at {f.dailyRateG.toFixed(1)}g/day</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-xs text-yellow-400 px-1">{t('forecast.noUsageQty')}</div>
|
||||
<div className="text-xs text-yellow-700 dark:text-yellow-400 px-1">{t('forecast.noUsageQty')}</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -56,10 +56,10 @@ function StatusBadge({ status }: StatusBadgeProps) {
|
||||
if (!status) return null;
|
||||
|
||||
const styles: Record<string, string> = {
|
||||
success: 'bg-green-500/20 text-green-400',
|
||||
failed: 'bg-red-500/20 text-red-400',
|
||||
skipped: 'bg-yellow-500/20 text-yellow-400',
|
||||
running: 'bg-blue-500/20 text-blue-400',
|
||||
success: 'bg-green-100 dark:bg-green-500/20 text-green-700 dark:text-green-400',
|
||||
failed: 'bg-red-100 dark:bg-red-500/20 text-red-700 dark:text-red-400',
|
||||
skipped: 'bg-yellow-100 dark:bg-yellow-500/20 text-yellow-700 dark:text-yellow-400',
|
||||
running: 'bg-blue-100 dark:bg-blue-500/20 text-blue-700 dark:text-blue-400',
|
||||
};
|
||||
|
||||
const icons: Record<string, React.ReactNode> = {
|
||||
@@ -658,7 +658,7 @@ export function GitHubBackupSettings() {
|
||||
{/* Access Token */}
|
||||
<div>
|
||||
<label className="block text-sm text-bambu-gray mb-1">
|
||||
{t('backup.personalAccessToken')} {config?.has_token && <span className="text-green-400">{t('backup.tokenSaved')}</span>}
|
||||
{t('backup.personalAccessToken')} {config?.has_token && <span className="text-green-700 dark:text-green-400">{t('backup.tokenSaved')}</span>}
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
@@ -722,13 +722,13 @@ export function GitHubBackupSettings() {
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={`text-sm ${noPrintersConnected ? 'text-bambu-gray' : 'text-white'}`}>{t('backup.kProfiles')}</span>
|
||||
{noPrintersConnected && (
|
||||
<span className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-xs bg-yellow-500/20 text-yellow-400">
|
||||
<span className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-xs bg-yellow-100 dark:bg-yellow-500/20 text-yellow-700 dark:text-yellow-400">
|
||||
<AlertTriangle className="w-3 h-3" />
|
||||
{t('backup.noPrintersConnected')}
|
||||
</span>
|
||||
)}
|
||||
{somePrintersDisconnected && (
|
||||
<span className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-xs bg-yellow-500/20 text-yellow-400">
|
||||
<span className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-xs bg-yellow-100 dark:bg-yellow-500/20 text-yellow-700 dark:text-yellow-400">
|
||||
<AlertTriangle className="w-3 h-3" />
|
||||
{t('backup.printersConnected', { connected: connectedPrinters, total: totalPrinters })}
|
||||
</span>
|
||||
@@ -749,7 +749,7 @@ export function GitHubBackupSettings() {
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={`text-sm ${cloudStatus?.is_authenticated ? 'text-white' : 'text-bambu-gray'}`}>{t('backup.cloudProfiles')}</span>
|
||||
{!cloudStatus?.is_authenticated && (
|
||||
<span className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-xs bg-yellow-500/20 text-yellow-400">
|
||||
<span className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-xs bg-yellow-100 dark:bg-yellow-500/20 text-yellow-700 dark:text-yellow-400">
|
||||
<AlertTriangle className="w-3 h-3" />
|
||||
{t('backup.cloudLoginRequiredShort')}
|
||||
</span>
|
||||
@@ -825,7 +825,7 @@ export function GitHubBackupSettings() {
|
||||
messages (e.g. the "repository is not private" guard)
|
||||
readable instead of clipped to a toast. */}
|
||||
{saveError && (
|
||||
<div className="text-sm text-red-400 bg-red-500/10 border border-red-500/30 rounded p-3 flex items-start gap-2">
|
||||
<div className="text-sm text-red-700 dark:text-red-400 bg-red-50 dark:bg-red-500/10 border border-red-300 dark:border-red-500/30 rounded p-3 flex items-start gap-2">
|
||||
<XCircle className="w-4 h-4 mt-0.5 shrink-0" />
|
||||
<div className="flex-1 leading-relaxed whitespace-pre-wrap break-words">{saveError}</div>
|
||||
<button
|
||||
@@ -842,18 +842,18 @@ export function GitHubBackupSettings() {
|
||||
{/* Test result */}
|
||||
{testResult && (
|
||||
<div className="space-y-1.5">
|
||||
<div className={`text-sm flex items-center gap-1 ${testResult.success ? 'text-green-400' : 'text-red-400'}`}>
|
||||
<div className={`text-sm flex items-center gap-1 ${testResult.success ? 'text-green-700 dark:text-green-400' : 'text-red-700 dark:text-red-400'}`}>
|
||||
{testResult.success ? <CheckCircle className="w-4 h-4" /> : <XCircle className="w-4 h-4" />}
|
||||
{testResult.message}
|
||||
</div>
|
||||
{testResult.success && testResult.isPrivate === true && (
|
||||
<div className="text-xs flex items-center gap-1 text-green-400">
|
||||
<div className="text-xs flex items-center gap-1 text-green-700 dark:text-green-400">
|
||||
<CheckCircle className="w-3.5 h-3.5" />
|
||||
{t('backup.repoIsPrivate', 'Repository is private — safe to back up to.')}
|
||||
</div>
|
||||
)}
|
||||
{testResult.success && testResult.isPrivate === false && (
|
||||
<div className="text-xs text-red-400 bg-red-500/10 border border-red-500/30 rounded p-2 flex items-start gap-1.5">
|
||||
<div className="text-xs text-red-700 dark:text-red-400 bg-red-50 dark:bg-red-500/10 border border-red-300 dark:border-red-500/30 rounded p-2 flex items-start gap-1.5">
|
||||
<XCircle className="w-4 h-4 mt-0.5 shrink-0" />
|
||||
<span>
|
||||
{t(
|
||||
@@ -864,7 +864,7 @@ export function GitHubBackupSettings() {
|
||||
</div>
|
||||
)}
|
||||
{testResult.success && testResult.isPrivate === null && (
|
||||
<div className="text-xs text-yellow-400 bg-yellow-500/10 border border-yellow-500/30 rounded p-2 flex items-start gap-1.5">
|
||||
<div className="text-xs text-yellow-700 dark:text-yellow-400 bg-yellow-50 dark:bg-yellow-500/10 border border-yellow-300 dark:border-yellow-500/30 rounded p-2 flex items-start gap-1.5">
|
||||
<XCircle className="w-4 h-4 mt-0.5 shrink-0" />
|
||||
<span>
|
||||
{t(
|
||||
@@ -1087,14 +1087,14 @@ export function GitHubBackupSettings() {
|
||||
|
||||
{/* Restore result message */}
|
||||
{restoreResult && (
|
||||
<div className={`p-3 rounded-lg ${restoreResult.success ? 'bg-green-500/10 border border-green-500/30' : 'bg-red-500/10 border border-red-500/30'}`}>
|
||||
<div className={`p-3 rounded-lg ${restoreResult.success ? 'bg-green-50 dark:bg-green-500/10 border border-green-300 dark:border-green-500/30' : 'bg-red-50 dark:bg-red-500/10 border border-red-300 dark:border-red-500/30'}`}>
|
||||
<div className="flex items-start gap-2 text-sm">
|
||||
{restoreResult.success ? (
|
||||
<CheckCircle className="w-4 h-4 text-green-400 mt-0.5 flex-shrink-0" />
|
||||
<CheckCircle className="w-4 h-4 text-green-600 dark:text-green-400 mt-0.5 flex-shrink-0" />
|
||||
) : (
|
||||
<XCircle className="w-4 h-4 text-red-400 mt-0.5 flex-shrink-0" />
|
||||
<XCircle className="w-4 h-4 text-red-600 dark:text-red-400 mt-0.5 flex-shrink-0" />
|
||||
)}
|
||||
<div className={restoreResult.success ? 'text-green-200' : 'text-red-200'}>
|
||||
<div className={restoreResult.success ? 'text-green-800 dark:text-green-200' : 'text-red-800 dark:text-red-200'}>
|
||||
{restoreResult.message}
|
||||
{restoreResult.success && (
|
||||
<div className="mt-2">
|
||||
@@ -1113,12 +1113,12 @@ export function GitHubBackupSettings() {
|
||||
)}
|
||||
|
||||
{/* Warning */}
|
||||
<div className="p-3 rounded-lg bg-yellow-500/10 border border-yellow-500/30">
|
||||
<div className="p-3 rounded-lg bg-yellow-50 dark:bg-yellow-500/10 border border-yellow-300 dark:border-yellow-500/30">
|
||||
<div className="flex items-start gap-2 text-sm">
|
||||
<AlertTriangle className="w-4 h-4 text-yellow-400 mt-0.5 flex-shrink-0" />
|
||||
<div className="text-yellow-200">
|
||||
<AlertTriangle className="w-4 h-4 text-yellow-600 dark:text-yellow-400 mt-0.5 flex-shrink-0" />
|
||||
<div className="text-yellow-800 dark:text-yellow-200">
|
||||
<span className="font-medium">{t('backup.restoreReplacesAll')}</span>{' '}
|
||||
<span className="text-yellow-200/70">{t('backup.restoreReplacesAllDetail')}</span>
|
||||
<span className="text-yellow-800/80 dark:text-yellow-200/70">{t('backup.restoreReplacesAllDetail')}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1318,14 +1318,14 @@ export function GitHubBackupSettings() {
|
||||
<Download className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
<button
|
||||
className="text-bambu-gray hover:text-yellow-400 p-1"
|
||||
className="text-bambu-gray hover:text-yellow-600 dark:hover:text-yellow-400 p-1"
|
||||
title={t('backup.restore')}
|
||||
onClick={() => setRestoreConfirmFile(file.filename)}
|
||||
>
|
||||
<RotateCcw className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
<button
|
||||
className="text-bambu-gray hover:text-red-400 p-1"
|
||||
className="text-bambu-gray hover:text-red-600 dark:hover:text-red-400 p-1"
|
||||
onClick={() => setDeleteConfirmFile(file.filename)}
|
||||
title={t('backup.deleteBackup')}
|
||||
>
|
||||
@@ -1425,10 +1425,10 @@ export function GitHubBackupSettings() {
|
||||
<p className="text-bambu-gray mb-4">
|
||||
{operationStatus || (isExporting ? t('backup.preparing') : t('backup.processing'))}
|
||||
</p>
|
||||
<div className="p-3 rounded-lg bg-yellow-500/10 border border-yellow-500/30">
|
||||
<div className="p-3 rounded-lg bg-yellow-50 dark:bg-yellow-500/10 border border-yellow-300 dark:border-yellow-500/30">
|
||||
<div className="flex items-start gap-2 text-sm">
|
||||
<AlertTriangle className="w-4 h-4 text-yellow-400 mt-0.5 flex-shrink-0" />
|
||||
<p className="text-yellow-200 text-left">
|
||||
<AlertTriangle className="w-4 h-4 text-yellow-600 dark:text-yellow-400 mt-0.5 flex-shrink-0" />
|
||||
<p className="text-yellow-800 dark:text-yellow-200 text-left">
|
||||
{t('backup.doNotClosePage')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -879,12 +879,12 @@ function getSeverityInfo(severity: number): { label: string; color: string; bgCo
|
||||
case 1:
|
||||
return { label: 'Fatal', color: 'text-red-500', bgColor: 'bg-red-500/20', Icon: AlertTriangle };
|
||||
case 2:
|
||||
return { label: 'Serious', color: 'text-red-400', bgColor: 'bg-red-500/15', Icon: AlertTriangle };
|
||||
return { label: 'Serious', color: 'text-red-700 dark:text-red-400', bgColor: 'bg-red-100 dark:bg-red-500/15', Icon: AlertTriangle };
|
||||
case 3:
|
||||
return { label: 'Warning', color: 'text-orange-400', bgColor: 'bg-orange-500/20', Icon: AlertCircle };
|
||||
return { label: 'Warning', color: 'text-orange-700 dark:text-orange-400', bgColor: 'bg-orange-100 dark:bg-orange-500/20', Icon: AlertCircle };
|
||||
case 4:
|
||||
default:
|
||||
return { label: 'Info', color: 'text-blue-400', bgColor: 'bg-blue-500/20', Icon: Info };
|
||||
return { label: 'Info', color: 'text-blue-700 dark:text-blue-400', bgColor: 'bg-blue-100 dark:bg-blue-500/20', Icon: Info };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -979,7 +979,7 @@ export function HMSErrorModal({ printerName, errors, onClose, printerId, hasPerm
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between p-4 border-b border-bambu-dark-tertiary">
|
||||
<div className="flex items-center gap-2">
|
||||
<AlertTriangle className="w-5 h-5 text-orange-400" />
|
||||
<AlertTriangle className="w-5 h-5 text-orange-600 dark:text-orange-400" />
|
||||
<h2 className="text-lg font-semibold text-white">{t('hmsErrors.title', { name: printerName })}</h2>
|
||||
</div>
|
||||
<button
|
||||
@@ -1089,7 +1089,7 @@ export function HMSErrorModal({ printerName, errors, onClose, printerId, hasPerm
|
||||
<button
|
||||
onClick={() => clearMutation.mutate()}
|
||||
disabled={!hasPermission('printers:control') || clearMutation.isPending}
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium rounded-lg bg-red-500/20 text-red-400 hover:bg-red-500/30 transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex-shrink-0"
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium rounded-lg bg-red-100 dark:bg-red-500/20 text-red-700 dark:text-red-400 hover:bg-red-200 dark:hover:bg-red-500/30 transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex-shrink-0"
|
||||
>
|
||||
{clearMutation.isPending ? (
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
|
||||
@@ -134,8 +134,8 @@ export function HeaterHistoryModal({
|
||||
const trend = getTrend(chartData.map(d => d.value));
|
||||
|
||||
const TrendIcon = ({ trend }: { trend: string }) => {
|
||||
if (trend === 'up') return <TrendingUp className="w-4 h-4 text-red-400" />;
|
||||
if (trend === 'down') return <TrendingDown className="w-4 h-4 text-green-400" />;
|
||||
if (trend === 'up') return <TrendingUp className="w-4 h-4 text-red-600 dark:text-red-400" />;
|
||||
if (trend === 'down') return <TrendingDown className="w-4 h-4 text-green-600 dark:text-green-400" />;
|
||||
return <Minus className="w-4 h-4 text-gray-400 dark:text-bambu-gray" />;
|
||||
};
|
||||
|
||||
@@ -264,7 +264,7 @@ export function HeaterHistoryModal({
|
||||
{t('common.loading', 'Loading...')}
|
||||
</div>
|
||||
) : error ? (
|
||||
<div className="h-64 flex items-center justify-center text-red-400">
|
||||
<div className="h-64 flex items-center justify-center text-red-700 dark:text-red-400">
|
||||
{t('printers.heaterHistory.error', 'Failed to load history')}
|
||||
</div>
|
||||
) : chartData.length === 0 ? (
|
||||
|
||||
@@ -205,14 +205,14 @@ export function LDAPSettings() {
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{ldapEnabled ? (
|
||||
<div className="bg-green-500/10 border border-green-500/30 rounded-lg p-4">
|
||||
<div className="bg-green-50 dark:bg-green-500/10 border border-green-300 dark:border-green-500/30 rounded-lg p-4">
|
||||
<div className="flex items-start gap-3">
|
||||
<CheckCircle className="w-5 h-5 text-green-400 mt-0.5 flex-shrink-0" />
|
||||
<CheckCircle className="w-5 h-5 text-green-600 dark:text-green-400 mt-0.5 flex-shrink-0" />
|
||||
<div className="space-y-2">
|
||||
<p className="text-white font-medium">
|
||||
{t('settings.ldap.enabledDesc') || 'LDAP authentication is enabled'}
|
||||
</p>
|
||||
<ul className="text-sm text-green-300 space-y-1 list-disc list-inside">
|
||||
<ul className="text-sm text-green-700 dark:text-green-300 space-y-1 list-disc list-inside">
|
||||
<li>{t('settings.ldap.feature1') || 'Users can login with LDAP credentials'}</li>
|
||||
<li>{t('settings.ldap.feature2') || 'Local admin account remains as fallback'}</li>
|
||||
<li>{t('settings.ldap.feature3') || 'LDAP groups are mapped to BamBuddy groups on login'}</li>
|
||||
@@ -221,14 +221,14 @@ export function LDAPSettings() {
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="bg-yellow-500/10 border border-yellow-500/30 rounded-lg p-4">
|
||||
<div className="bg-yellow-50 dark:bg-yellow-500/10 border border-yellow-300 dark:border-yellow-500/30 rounded-lg p-4">
|
||||
<div className="flex items-start gap-3">
|
||||
<AlertTriangle className="w-5 h-5 text-yellow-400 mt-0.5 flex-shrink-0" />
|
||||
<AlertTriangle className="w-5 h-5 text-yellow-600 dark:text-yellow-400 mt-0.5 flex-shrink-0" />
|
||||
<div>
|
||||
<p className="text-white font-medium">
|
||||
{t('settings.ldap.disabledDesc') || 'LDAP authentication is disabled'}
|
||||
</p>
|
||||
<p className="text-sm text-yellow-300 mt-1">
|
||||
<p className="text-sm text-yellow-700 dark:text-yellow-300 mt-1">
|
||||
{t('settings.ldap.disabledHint') || 'Configure and save LDAP settings below, then enable.'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -843,20 +843,20 @@ export function Layout() {
|
||||
}`}>
|
||||
{/* Debug logging indicator */}
|
||||
{debugLoggingState?.enabled && (
|
||||
<div className="bg-amber-500/20 border-b border-amber-500/30 px-4 py-2 flex items-center justify-between">
|
||||
<div className="bg-amber-100 dark:bg-amber-500/20 border-b border-amber-300 dark:border-amber-500/30 px-4 py-2 flex items-center justify-between">
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<Bug className="w-4 h-4 text-amber-500 animate-pulse" />
|
||||
<span className="text-amber-200">
|
||||
<span className="text-amber-800 dark:text-amber-200">
|
||||
{t('support.debugLoggingActive', { defaultValue: 'Debug logging is active' })}
|
||||
{debugDuration !== null && (
|
||||
<span className="text-amber-300/70 ml-2">
|
||||
<span className="text-amber-700/80 dark:text-amber-300/70 ml-2">
|
||||
({Math.floor(debugDuration / 60)}m {debugDuration % 60}s)
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => navigate('/system')}
|
||||
className="text-amber-400 hover:text-amber-300 font-medium underline ml-2"
|
||||
className="text-amber-700 dark:text-amber-400 hover:text-amber-900 dark:hover:text-amber-300 font-medium underline ml-2"
|
||||
>
|
||||
{t('support.manageLogs', { defaultValue: 'Manage' })}
|
||||
</button>
|
||||
@@ -864,10 +864,10 @@ export function Layout() {
|
||||
</div>
|
||||
)}
|
||||
{devModeWarnings && devModeWarnings.length > 0 && (
|
||||
<div className="bg-orange-500/20 border-b border-orange-500/30 px-4 py-2 flex items-center justify-between">
|
||||
<div className="bg-orange-100 dark:bg-orange-500/20 border-b border-orange-300 dark:border-orange-500/30 px-4 py-2 flex items-center justify-between">
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<ShieldAlert className="w-4 h-4 text-orange-500" />
|
||||
<span className="text-orange-200">
|
||||
<span className="text-orange-800 dark:text-orange-200">
|
||||
{t('printers.developerModeWarning', {
|
||||
names: devModeWarnings.map(w => w.name).join(', '),
|
||||
defaultValue: `Developer LAN mode is not enabled on: ${devModeWarnings.map(w => w.name).join(', ')}. Some features may not work.`
|
||||
@@ -875,7 +875,7 @@ export function Layout() {
|
||||
</span>
|
||||
<a href="https://wiki.bambulab.com/en/knowledge-sharing/enable-developer-mode"
|
||||
target="_blank" rel="noopener noreferrer"
|
||||
className="text-orange-400 hover:text-orange-300 font-medium underline ml-2">
|
||||
className="text-orange-700 dark:text-orange-400 hover:text-orange-900 dark:hover:text-orange-300 font-medium underline ml-2">
|
||||
{t('printers.howToEnable', { defaultValue: 'How to enable' })}
|
||||
</a>
|
||||
</div>
|
||||
@@ -944,7 +944,7 @@ export function Layout() {
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h2 className="text-xl font-bold text-yellow-400 mb-2">
|
||||
<h2 className="text-xl font-bold text-yellow-700 dark:text-yellow-400 mb-2">
|
||||
{t('plateAlert.title')}
|
||||
</h2>
|
||||
<p className="text-lg text-white mb-2">
|
||||
@@ -1052,7 +1052,7 @@ export function Layout() {
|
||||
minLength={6}
|
||||
/>
|
||||
{changePasswordData.confirmPassword && changePasswordData.newPassword !== changePasswordData.confirmPassword && (
|
||||
<p className="text-red-400 text-xs mt-1">{t('changePassword.passwordsDoNotMatch')}</p>
|
||||
<p className="text-red-700 dark:text-red-400 text-xs mt-1">{t('changePassword.passwordsDoNotMatch')}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -115,7 +115,7 @@ export function LdapUserPicker({ onSuccess }: LdapUserPickerProps) {
|
||||
)}
|
||||
|
||||
{searchQuery.isError && (
|
||||
<div className="px-3 py-4 text-sm text-red-400">
|
||||
<div className="px-3 py-4 text-sm text-red-700 dark:text-red-400">
|
||||
{searchQuery.error instanceof Error ? searchQuery.error.message : t('users.modal.ldapSearchError')}
|
||||
</div>
|
||||
)}
|
||||
@@ -159,8 +159,8 @@ export function LdapUserPicker({ onSuccess }: LdapUserPickerProps) {
|
||||
|
||||
{/* Error from the provision mutation */}
|
||||
{errorMessage && (
|
||||
<div className="bg-red-500/10 border border-red-500/30 rounded-lg p-3">
|
||||
<p className="text-sm text-red-400">{errorMessage}</p>
|
||||
<div className="bg-red-50 dark:bg-red-500/10 border border-red-300 dark:border-red-500/30 rounded-lg p-3">
|
||||
<p className="text-sm text-red-700 dark:text-red-400">{errorMessage}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -206,7 +206,7 @@ export function LibraryTagsModal({ open, onClose, onPickTag }: LibraryTagsModalP
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="p-1.5 text-bambu-gray hover:text-red-400 rounded"
|
||||
className="p-1.5 text-bambu-gray hover:text-red-600 dark:hover:text-red-400 rounded"
|
||||
onClick={() => setDeleteTarget(tag)}
|
||||
title={t('common.delete')}
|
||||
aria-label={t('fileManager.tags.deleteAria', { name: tag.name })}
|
||||
|
||||
@@ -127,7 +127,7 @@ function PresetCard({
|
||||
{vendor && (
|
||||
<span className="text-xs text-bambu-gray">{vendor}</span>
|
||||
)}
|
||||
<span className="text-xs px-1.5 py-0.5 rounded bg-blue-500/20 text-blue-400">
|
||||
<span className="text-xs px-1.5 py-0.5 rounded bg-blue-100 dark:bg-blue-500/20 text-blue-700 dark:text-blue-400">
|
||||
{t('profiles.localProfiles.badge')}
|
||||
</span>
|
||||
</div>
|
||||
@@ -138,7 +138,7 @@ function PresetCard({
|
||||
{hasPermission('settings:update') && (
|
||||
<button
|
||||
onClick={() => onDelete(preset.id)}
|
||||
className="p-1 text-bambu-gray hover:text-red-400 transition-colors"
|
||||
className="p-1 text-bambu-gray hover:text-red-600 dark:hover:text-red-400 transition-colors"
|
||||
title={t('profiles.localProfiles.delete')}
|
||||
>
|
||||
<Trash2 className="w-3.5 h-3.5" />
|
||||
@@ -443,7 +443,7 @@ export function LocalProfilesView() {
|
||||
{processes.length > 0 && (
|
||||
<div>
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<Layers className="w-4 h-4 text-blue-400" />
|
||||
<Layers className="w-4 h-4 text-blue-600 dark:text-blue-400" />
|
||||
<h3 className="text-sm font-medium text-white">
|
||||
{t('profiles.localProfiles.process')}
|
||||
</h3>
|
||||
@@ -467,7 +467,7 @@ export function LocalProfilesView() {
|
||||
{printers.length > 0 && (
|
||||
<div>
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<Settings2 className="w-4 h-4 text-orange-400" />
|
||||
<Settings2 className="w-4 h-4 text-orange-600 dark:text-orange-400" />
|
||||
<h3 className="text-sm font-medium text-white">
|
||||
{t('profiles.localProfiles.printer')}
|
||||
</h3>
|
||||
@@ -494,7 +494,7 @@ export function LocalProfilesView() {
|
||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
|
||||
<div className="bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-lg p-6 max-w-sm mx-4">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<AlertCircle className="w-5 h-5 text-red-400" />
|
||||
<AlertCircle className="w-5 h-5 text-red-600 dark:text-red-400" />
|
||||
<h3 className="text-white font-medium">{t('profiles.localProfiles.deleteConfirmTitle')}</h3>
|
||||
</div>
|
||||
<p className="text-sm text-bambu-gray mb-4">{t('profiles.localProfiles.deleteConfirm')}</p>
|
||||
|
||||
@@ -200,7 +200,7 @@ export function LocationsModal({ open, onClose, onPickLocation }: LocationsModal
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="p-1.5 text-bambu-gray hover:text-red-400 rounded disabled:opacity-40"
|
||||
className="p-1.5 text-bambu-gray hover:text-red-600 dark:hover:text-red-400 rounded disabled:opacity-40"
|
||||
disabled={loc.spool_count > 0}
|
||||
onClick={() => setDeleteTarget(loc)}
|
||||
title={loc.spool_count > 0 ? t('locations.deleteBlocked') : t('common.delete')}
|
||||
|
||||
@@ -162,7 +162,7 @@ export function LogViewer() {
|
||||
e.stopPropagation();
|
||||
setIsStreaming(false);
|
||||
}}
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 text-sm bg-red-500/20 text-red-400 hover:bg-red-500/30 rounded transition-colors"
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 text-sm bg-red-100 dark:bg-red-500/20 text-red-700 dark:text-red-400 hover:bg-red-200 dark:hover:bg-red-500/30 rounded transition-colors"
|
||||
>
|
||||
<Square className="w-4 h-4" />
|
||||
Stop
|
||||
|
||||
@@ -9,15 +9,15 @@ import { Button } from './Button';
|
||||
import { useToast } from '../contexts/ToastContext';
|
||||
|
||||
const EVENT_COLORS: Record<string, string> = {
|
||||
print_start: 'text-blue-400',
|
||||
print_start: 'text-blue-700 dark:text-blue-400',
|
||||
print_complete: 'text-bambu-green',
|
||||
print_failed: 'text-red-400',
|
||||
print_stopped: 'text-orange-400',
|
||||
print_progress: 'text-yellow-400',
|
||||
print_failed: 'text-red-700 dark:text-red-400',
|
||||
print_stopped: 'text-orange-700 dark:text-orange-400',
|
||||
print_progress: 'text-yellow-700 dark:text-yellow-400',
|
||||
printer_offline: 'text-gray-400',
|
||||
printer_error: 'text-rose-400',
|
||||
filament_low: 'text-cyan-400',
|
||||
maintenance_due: 'text-purple-400',
|
||||
printer_error: 'text-rose-700 dark:text-rose-400',
|
||||
filament_low: 'text-cyan-700 dark:text-cyan-400',
|
||||
maintenance_due: 'text-purple-700 dark:text-purple-400',
|
||||
test: 'text-bambu-gray',
|
||||
};
|
||||
|
||||
@@ -108,7 +108,7 @@ export function NotificationLogViewer({ onClose }: NotificationLogViewerProps) {
|
||||
{t('notifications.statsSent', { count: stats.success_count })}
|
||||
</span>
|
||||
{stats.failure_count > 0 && (
|
||||
<span className="flex items-center gap-1 text-red-400">
|
||||
<span className="flex items-center gap-1 text-red-700 dark:text-red-400">
|
||||
<XCircle className="w-4 h-4" />
|
||||
{t('notifications.statsFailed', { count: stats.failure_count })}
|
||||
</span>
|
||||
@@ -161,7 +161,7 @@ export function NotificationLogViewer({ onClose }: NotificationLogViewerProps) {
|
||||
variant="secondary"
|
||||
onClick={() => clearMutation.mutate()}
|
||||
disabled={clearMutation.isPending}
|
||||
className="text-red-400 hover:text-red-300"
|
||||
className="text-red-700 dark:text-red-400 hover:text-red-800 dark:hover:text-red-300"
|
||||
>
|
||||
{clearMutation.isPending ? (
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
@@ -234,7 +234,7 @@ function LogEntry({
|
||||
{log.success ? (
|
||||
<CheckCircle className="w-4 h-4 text-bambu-green shrink-0" />
|
||||
) : (
|
||||
<XCircle className="w-4 h-4 text-red-400 shrink-0" />
|
||||
<XCircle className="w-4 h-4 text-red-600 dark:text-red-400 shrink-0" />
|
||||
)}
|
||||
|
||||
<span className={`text-xs font-medium ${EVENT_COLORS[log.event_type] || 'text-bambu-gray'}`}>
|
||||
@@ -274,8 +274,8 @@ function LogEntry({
|
||||
</div>
|
||||
{!log.success && log.error_message && (
|
||||
<div>
|
||||
<p className="text-xs text-red-400 mb-1">{t('notifications.logError')}</p>
|
||||
<p className="text-sm text-red-300">{log.error_message}</p>
|
||||
<p className="text-xs text-red-700 dark:text-red-400 mb-1">{t('notifications.logError')}</p>
|
||||
<p className="text-sm text-red-700 dark:text-red-300">{log.error_message}</p>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex gap-4 text-xs text-bambu-gray pt-1">
|
||||
|
||||
@@ -115,73 +115,73 @@ export function NotificationProviderCard({ provider, onEdit }: NotificationProvi
|
||||
{/* Event summary - show all event tags */}
|
||||
<div className="mb-3 flex flex-wrap gap-1">
|
||||
{provider.on_print_start && (
|
||||
<span className="px-2 py-0.5 bg-blue-500/20 text-blue-400 text-xs rounded">{t('notifications.start')}</span>
|
||||
<span className="px-2 py-0.5 bg-blue-100 dark:bg-blue-500/20 text-blue-700 dark:text-blue-400 text-xs rounded">{t('notifications.start')}</span>
|
||||
)}
|
||||
{provider.on_plate_not_empty && (
|
||||
<span className="px-2 py-0.5 bg-rose-600/20 text-rose-300 text-xs rounded">{t('notifications.plateCheck')}</span>
|
||||
<span className="px-2 py-0.5 bg-rose-100 dark:bg-rose-600/20 text-rose-700 dark:text-rose-300 text-xs rounded">{t('notifications.plateCheck')}</span>
|
||||
)}
|
||||
{provider.on_print_complete && (
|
||||
<span className="px-2 py-0.5 bg-bambu-green/20 text-bambu-green text-xs rounded">{t('notifications.complete')}</span>
|
||||
)}
|
||||
{provider.on_print_failed && (
|
||||
<span className="px-2 py-0.5 bg-red-500/20 text-red-400 text-xs rounded">{t('notifications.failed')}</span>
|
||||
<span className="px-2 py-0.5 bg-red-100 dark:bg-red-500/20 text-red-700 dark:text-red-400 text-xs rounded">{t('notifications.failed')}</span>
|
||||
)}
|
||||
{provider.on_print_stopped && (
|
||||
<span className="px-2 py-0.5 bg-orange-500/20 text-orange-400 text-xs rounded">{t('notifications.stopped')}</span>
|
||||
<span className="px-2 py-0.5 bg-orange-100 dark:bg-orange-500/20 text-orange-700 dark:text-orange-400 text-xs rounded">{t('notifications.stopped')}</span>
|
||||
)}
|
||||
{provider.on_print_progress && (
|
||||
<span className="px-2 py-0.5 bg-yellow-500/20 text-yellow-400 text-xs rounded">{t('notifications.progress')}</span>
|
||||
<span className="px-2 py-0.5 bg-yellow-100 dark:bg-yellow-500/20 text-yellow-700 dark:text-yellow-400 text-xs rounded">{t('notifications.progress')}</span>
|
||||
)}
|
||||
{provider.on_printer_offline && (
|
||||
<span className="px-2 py-0.5 bg-gray-500/20 text-gray-400 text-xs rounded">{t('notifications.offline')}</span>
|
||||
)}
|
||||
{provider.on_printer_error && (
|
||||
<span className="px-2 py-0.5 bg-rose-500/20 text-rose-400 text-xs rounded">{t('notifications.error')}</span>
|
||||
<span className="px-2 py-0.5 bg-rose-100 dark:bg-rose-500/20 text-rose-700 dark:text-rose-400 text-xs rounded">{t('notifications.error')}</span>
|
||||
)}
|
||||
{provider.on_ai_failure_detection && (
|
||||
<span className="px-2 py-0.5 bg-fuchsia-500/20 text-fuchsia-300 text-xs rounded">{t('notifications.aiFailureDetection')}</span>
|
||||
<span className="px-2 py-0.5 bg-fuchsia-100 dark:bg-fuchsia-500/20 text-fuchsia-700 dark:text-fuchsia-300 text-xs rounded">{t('notifications.aiFailureDetection')}</span>
|
||||
)}
|
||||
{provider.on_filament_low && (
|
||||
<span className="px-2 py-0.5 bg-cyan-500/20 text-cyan-400 text-xs rounded">{t('notifications.lowFilament')}</span>
|
||||
<span className="px-2 py-0.5 bg-cyan-100 dark:bg-cyan-500/20 text-cyan-700 dark:text-cyan-400 text-xs rounded">{t('notifications.lowFilament')}</span>
|
||||
)}
|
||||
{provider.on_maintenance_due && (
|
||||
<span className="px-2 py-0.5 bg-purple-500/20 text-purple-400 text-xs rounded">{t('notifications.maintenance')}</span>
|
||||
<span className="px-2 py-0.5 bg-purple-100 dark:bg-purple-500/20 text-purple-700 dark:text-purple-400 text-xs rounded">{t('notifications.maintenance')}</span>
|
||||
)}
|
||||
{provider.on_ams_humidity_high && (
|
||||
<span className="px-2 py-0.5 bg-blue-600/20 text-blue-300 text-xs rounded">{t('notifications.amsHumidity')}</span>
|
||||
<span className="px-2 py-0.5 bg-blue-100 dark:bg-blue-600/20 text-blue-700 dark:text-blue-300 text-xs rounded">{t('notifications.amsHumidity')}</span>
|
||||
)}
|
||||
{provider.on_ams_temperature_high && (
|
||||
<span className="px-2 py-0.5 bg-orange-600/20 text-orange-300 text-xs rounded">{t('notifications.amsTemp')}</span>
|
||||
<span className="px-2 py-0.5 bg-orange-100 dark:bg-orange-600/20 text-orange-700 dark:text-orange-300 text-xs rounded">{t('notifications.amsTemp')}</span>
|
||||
)}
|
||||
{provider.on_ams_ht_humidity_high && (
|
||||
<span className="px-2 py-0.5 bg-cyan-600/20 text-cyan-300 text-xs rounded">{t('notifications.amsHtHumidity')}</span>
|
||||
<span className="px-2 py-0.5 bg-cyan-100 dark:bg-cyan-600/20 text-cyan-700 dark:text-cyan-300 text-xs rounded">{t('notifications.amsHtHumidity')}</span>
|
||||
)}
|
||||
{provider.on_ams_ht_temperature_high && (
|
||||
<span className="px-2 py-0.5 bg-amber-600/20 text-amber-300 text-xs rounded">{t('notifications.amsHtTemp')}</span>
|
||||
<span className="px-2 py-0.5 bg-amber-100 dark:bg-amber-600/20 text-amber-700 dark:text-amber-300 text-xs rounded">{t('notifications.amsHtTemp')}</span>
|
||||
)}
|
||||
{provider.on_bed_cooled && (
|
||||
<span className="px-2 py-0.5 bg-teal-500/20 text-teal-400 text-xs rounded">{t('notifications.bedCooled')}</span>
|
||||
<span className="px-2 py-0.5 bg-teal-100 dark:bg-teal-500/20 text-teal-700 dark:text-teal-400 text-xs rounded">{t('notifications.bedCooled')}</span>
|
||||
)}
|
||||
{provider.on_first_layer_complete && (
|
||||
<span className="px-2 py-0.5 bg-emerald-600/20 text-emerald-300 text-xs rounded">{t('notifications.firstLayer')}</span>
|
||||
<span className="px-2 py-0.5 bg-emerald-100 dark:bg-emerald-600/20 text-emerald-700 dark:text-emerald-300 text-xs rounded">{t('notifications.firstLayer')}</span>
|
||||
)}
|
||||
{provider.on_print_missing_spool_assignment && (
|
||||
<span className="px-2 py-0.5 bg-amber-500/20 text-amber-300 text-xs rounded">{t('notifications.missingSpoolAssignmentLabel')}</span>
|
||||
<span className="px-2 py-0.5 bg-amber-100 dark:bg-amber-500/20 text-amber-700 dark:text-amber-300 text-xs rounded">{t('notifications.missingSpoolAssignmentLabel')}</span>
|
||||
)}
|
||||
{provider.on_stock_reorder_alert && (
|
||||
<span className="px-2 py-0.5 bg-lime-500/20 text-lime-400 text-xs rounded">{t('notifications.stockReorderAlert')}</span>
|
||||
<span className="px-2 py-0.5 bg-lime-100 dark:bg-lime-500/20 text-lime-700 dark:text-lime-400 text-xs rounded">{t('notifications.stockReorderAlert')}</span>
|
||||
)}
|
||||
{provider.on_stock_break_alert && (
|
||||
<span className="px-2 py-0.5 bg-red-600/20 text-red-300 text-xs rounded">{t('notifications.stockBreakAlert')}</span>
|
||||
<span className="px-2 py-0.5 bg-red-100 dark:bg-red-600/20 text-red-700 dark:text-red-300 text-xs rounded">{t('notifications.stockBreakAlert')}</span>
|
||||
)}
|
||||
{provider.quiet_hours_enabled && (
|
||||
<span className="px-2 py-0.5 bg-indigo-500/20 text-indigo-400 text-xs rounded flex items-center gap-1">
|
||||
<span className="px-2 py-0.5 bg-indigo-100 dark:bg-indigo-500/20 text-indigo-700 dark:text-indigo-400 text-xs rounded flex items-center gap-1">
|
||||
<Moon className="w-3 h-3" />
|
||||
{t('notifications.quiet')}
|
||||
</span>
|
||||
)}
|
||||
{provider.daily_digest_enabled && (
|
||||
<span className="px-2 py-0.5 bg-emerald-500/20 text-emerald-400 text-xs rounded flex items-center gap-1">
|
||||
<span className="px-2 py-0.5 bg-emerald-100 dark:bg-emerald-500/20 text-emerald-700 dark:text-emerald-400 text-xs rounded flex items-center gap-1">
|
||||
<Calendar className="w-3 h-3" />
|
||||
{t('notifications.digest', { time: provider.daily_digest_time })}
|
||||
</span>
|
||||
@@ -214,7 +214,7 @@ export function NotificationProviderCard({ provider, onEdit }: NotificationProvi
|
||||
<div className={`mb-3 p-2 rounded-lg flex items-center gap-2 text-sm ${
|
||||
testResult.success
|
||||
? 'bg-bambu-green/20 text-bambu-green'
|
||||
: 'bg-red-500/20 text-red-400'
|
||||
: 'bg-red-100 dark:bg-red-500/20 text-red-700 dark:text-red-400'
|
||||
}`}>
|
||||
{testResult.success ? (
|
||||
<CheckCircle className="w-4 h-4" />
|
||||
@@ -567,7 +567,7 @@ export function NotificationProviderCard({ provider, onEdit }: NotificationProvi
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Moon className="w-4 h-4 text-purple-400" />
|
||||
<Moon className="w-4 h-4 text-purple-600 dark:text-purple-400" />
|
||||
<p className="text-sm text-white">{t('notifications.quietHours')}</p>
|
||||
</div>
|
||||
<Toggle
|
||||
@@ -594,7 +594,7 @@ export function NotificationProviderCard({ provider, onEdit }: NotificationProvi
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Calendar className="w-4 h-4 text-emerald-400" />
|
||||
<Calendar className="w-4 h-4 text-emerald-600 dark:text-emerald-400" />
|
||||
<p className="text-sm text-white">{t('notifications.dailyDigest')}</p>
|
||||
</div>
|
||||
<Toggle
|
||||
@@ -632,7 +632,7 @@ export function NotificationProviderCard({ provider, onEdit }: NotificationProvi
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
onClick={() => setShowDeleteConfirm(true)}
|
||||
className="text-red-400 hover:text-red-300"
|
||||
className="text-red-700 dark:text-red-400 hover:text-red-800 dark:hover:text-red-300"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</Button>
|
||||
|
||||
@@ -136,7 +136,7 @@ export function NotificationTemplateEditor({ template, onClose }: NotificationTe
|
||||
{/* Content */}
|
||||
<form onSubmit={handleSubmit} className="flex-1 overflow-y-auto p-4 space-y-4">
|
||||
{error && (
|
||||
<div className="p-3 bg-red-500/20 border border-red-500/50 rounded text-red-400 text-sm">
|
||||
<div className="p-3 bg-red-100 dark:bg-red-500/20 border border-red-300 dark:border-red-500/50 rounded text-red-700 dark:text-red-400 text-sm">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
@@ -245,7 +245,7 @@ export function NotificationTemplateEditor({ template, onClose }: NotificationTe
|
||||
variant="ghost"
|
||||
onClick={() => resetMutation.mutate()}
|
||||
disabled={resetMutation.isPending}
|
||||
className="text-orange-400 hover:text-orange-300"
|
||||
className="text-orange-600 dark:text-orange-400 hover:text-orange-700 dark:hover:text-orange-300"
|
||||
>
|
||||
{resetMutation.isPending ? (
|
||||
<Loader2 className="w-4 h-4 animate-spin mr-2" />
|
||||
|
||||
@@ -69,7 +69,7 @@ function ProviderForm({
|
||||
requireEmailVerifiedDesc = t('settings.oidc.form.requireEmailVerifiedDesc');
|
||||
} else {
|
||||
requireEmailVerifiedDesc = (
|
||||
<span className="text-red-400">{t('settings.oidc.form.requireEmailVerifiedWarning')}</span>
|
||||
<span className="text-red-700 dark:text-red-400">{t('settings.oidc.form.requireEmailVerifiedWarning')}</span>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -77,21 +77,21 @@ function ProviderForm({
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className={labelCls}>{t('settings.oidc.form.name')} <span className="text-red-400">*</span></label>
|
||||
<label className={labelCls}>{t('settings.oidc.form.name')} <span className="text-red-700 dark:text-red-400">*</span></label>
|
||||
<input className={inputCls} value={form.name} onChange={(e) => set('name', e.target.value)} placeholder="Google" />
|
||||
</div>
|
||||
<div>
|
||||
<label className={labelCls}>{t('settings.oidc.form.issuerUrl')} <span className="text-red-400">*</span></label>
|
||||
<label className={labelCls}>{t('settings.oidc.form.issuerUrl')} <span className="text-red-700 dark:text-red-400">*</span></label>
|
||||
<input className={inputCls} value={form.issuer_url} onChange={(e) => set('issuer_url', e.target.value)} placeholder="https://accounts.google.com" />
|
||||
</div>
|
||||
<div>
|
||||
<label className={labelCls}>{t('settings.oidc.form.clientId')} <span className="text-red-400">*</span></label>
|
||||
<label className={labelCls}>{t('settings.oidc.form.clientId')} <span className="text-red-700 dark:text-red-400">*</span></label>
|
||||
<input className={inputCls} value={form.client_id} onChange={(e) => set('client_id', e.target.value)} placeholder="your-client-id" />
|
||||
</div>
|
||||
<div>
|
||||
<label className={labelCls}>
|
||||
{t('settings.oidc.form.clientSecret')}
|
||||
{!isEdit && <span className="text-red-400"> *</span>}
|
||||
{!isEdit && <span className="text-red-700 dark:text-red-400"> *</span>}
|
||||
{isEdit && <span className="text-bambu-gray text-xs ml-1">({t('settings.oidc.form.secretHint')})</span>}
|
||||
</label>
|
||||
<input
|
||||
@@ -169,7 +169,7 @@ function ProviderForm({
|
||||
/>
|
||||
<p className="text-bambu-gray text-xs mt-1">{t('settings.oidc.form.emailClaimDesc')}</p>
|
||||
{autoLinkOn && form.email_claim !== 'email' && (
|
||||
<p className="text-yellow-400 text-xs mt-1">{t('settings.oidc.form.emailClaimCustomClaimAutoLinkWarning')}</p>
|
||||
<p className="text-yellow-700 dark:text-yellow-400 text-xs mt-1">{t('settings.oidc.form.emailClaimCustomClaimAutoLinkWarning')}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -378,7 +378,7 @@ export function OIDCProviderSettings() {
|
||||
<div className="flex items-center gap-2">
|
||||
<h4 className="text-white font-medium">{provider.name}</h4>
|
||||
{provider.is_enabled ? (
|
||||
<span className="flex items-center gap-1 text-xs text-green-400 bg-green-400/10 px-2 py-0.5 rounded-full">
|
||||
<span className="flex items-center gap-1 text-xs text-green-700 dark:text-green-400 bg-green-50 dark:bg-green-400/10 px-2 py-0.5 rounded-full">
|
||||
<Check className="w-3 h-3" /> {t('common.enabled')}
|
||||
</span>
|
||||
) : (
|
||||
@@ -478,13 +478,13 @@ export function OIDCProviderSettings() {
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-bambu-gray">{t('settings.oidc.form.autoCreate')}</dt>
|
||||
<dd className={provider.auto_create_users ? 'text-green-400' : 'text-bambu-gray'}>
|
||||
<dd className={provider.auto_create_users ? 'text-green-700 dark:text-green-400' : 'text-bambu-gray'}>
|
||||
{provider.auto_create_users ? t('common.yes') : t('common.no')}
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-bambu-gray">{t('settings.oidc.form.autoLink')}</dt>
|
||||
<dd className={provider.auto_link_existing_accounts ? 'text-green-400' : 'text-bambu-gray'}>
|
||||
<dd className={provider.auto_link_existing_accounts ? 'text-green-700 dark:text-green-400' : 'text-bambu-gray'}>
|
||||
{provider.auto_link_existing_accounts ? t('common.yes') : t('common.no')}
|
||||
</dd>
|
||||
</div>
|
||||
@@ -494,7 +494,7 @@ export function OIDCProviderSettings() {
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-bambu-gray">{t('settings.oidc.form.requireEmailVerified')}</dt>
|
||||
<dd className={provider.require_email_verified ? 'text-green-400' : 'text-red-400'}>
|
||||
<dd className={provider.require_email_verified ? 'text-green-700 dark:text-green-400' : 'text-red-700 dark:text-red-400'}>
|
||||
{provider.require_email_verified ? t('common.yes') : t('common.no')}
|
||||
</dd>
|
||||
</div>
|
||||
|
||||
@@ -257,21 +257,21 @@ export function OrcaCloudProfilesView({
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<PresetColumn
|
||||
icon={<Droplet className="w-4 h-4 text-amber-400" />}
|
||||
icon={<Droplet className="w-4 h-4 text-amber-600 dark:text-amber-400" />}
|
||||
title={t('profiles.cloudView.columns.filament')}
|
||||
presets={presetsByType('filament')}
|
||||
emptyText={t('profiles.cloudView.noFilamentPresets')}
|
||||
onSelect={setSelectedSetting}
|
||||
/>
|
||||
<PresetColumn
|
||||
icon={<Settings2 className="w-4 h-4 text-blue-400" />}
|
||||
icon={<Settings2 className="w-4 h-4 text-blue-600 dark:text-blue-400" />}
|
||||
title={t('profiles.cloudView.columns.process')}
|
||||
presets={presetsByType('process')}
|
||||
emptyText={t('profiles.cloudView.noProcessPresets')}
|
||||
onSelect={setSelectedSetting}
|
||||
/>
|
||||
<PresetColumn
|
||||
icon={<PrinterIcon className="w-4 h-4 text-purple-400" />}
|
||||
icon={<PrinterIcon className="w-4 h-4 text-purple-600 dark:text-purple-400" />}
|
||||
title={t('profiles.cloudView.columns.printer')}
|
||||
presets={presetsByType('printer')}
|
||||
emptyText={t('profiles.cloudView.noPrinterPresets')}
|
||||
|
||||
@@ -387,10 +387,10 @@ function PasteCard({
|
||||
<p className="text-base text-white pt-0.5">{t('profiles.orcaCloud.paste.step1')}</p>
|
||||
</li>
|
||||
<li className="flex gap-3">
|
||||
<span className="flex-shrink-0 w-7 h-7 rounded-full bg-amber-500/20 text-amber-400 text-sm font-bold flex items-center justify-center">2</span>
|
||||
<span className="flex-shrink-0 w-7 h-7 rounded-full bg-amber-100 dark:bg-amber-500/20 text-amber-700 dark:text-amber-400 text-sm font-bold flex items-center justify-center">2</span>
|
||||
<div className="flex-1 p-3 bg-amber-500/10 border border-amber-500/40 rounded">
|
||||
<div className="flex items-start gap-2">
|
||||
<AlertTriangle className="w-5 h-5 text-amber-400 flex-shrink-0 mt-0.5" />
|
||||
<AlertTriangle className="w-5 h-5 text-amber-600 dark:text-amber-400 flex-shrink-0 mt-0.5" />
|
||||
<p className="text-base text-white font-medium">{t('profiles.orcaCloud.paste.step2')}</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -426,7 +426,7 @@ function PasteCard({
|
||||
disabled={isFinishing}
|
||||
/>
|
||||
{pasteError && (
|
||||
<p className="mt-2 text-sm text-red-400 flex items-center gap-2">
|
||||
<p className="mt-2 text-sm text-red-700 dark:text-red-400 flex items-center gap-2">
|
||||
<AlertCircle className="w-4 h-4" />
|
||||
{pasteError}
|
||||
</p>
|
||||
@@ -499,7 +499,7 @@ function PasswordCard({
|
||||
/>
|
||||
</div>
|
||||
{passwordError && (
|
||||
<p className="text-sm text-red-400 flex items-center gap-2">
|
||||
<p className="text-sm text-red-700 dark:text-red-400 flex items-center gap-2">
|
||||
<AlertCircle className="w-4 h-4" />
|
||||
{passwordError}
|
||||
</p>
|
||||
|
||||
@@ -102,7 +102,7 @@ function PendingUploadItem({
|
||||
{isDiscarding ? (
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
) : (
|
||||
<Trash2 className="w-4 h-4 text-red-400" />
|
||||
<Trash2 className="w-4 h-4 text-red-600 dark:text-red-400" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -64,7 +64,7 @@ export function PrintLogTable({ archiveId }: PrintLogTableProps) {
|
||||
run.status === 'completed'
|
||||
? 'text-bambu-green'
|
||||
: run.status === 'failed'
|
||||
? 'text-red-400'
|
||||
? 'text-red-700 dark:text-red-400'
|
||||
: 'text-bambu-gray';
|
||||
return (
|
||||
<tr
|
||||
|
||||
@@ -194,9 +194,9 @@ export function FilamentMapping({
|
||||
<Circle className="w-4 h-4" fill={statusColor} stroke="none" />
|
||||
<span>{t('printModal.filamentMapping')}</span>
|
||||
{hasTypeMismatch ? (
|
||||
<span className="text-xs text-orange-400">(Type not found)</span>
|
||||
<span className="text-xs text-orange-700 dark:text-orange-400">(Type not found)</span>
|
||||
) : hasColorMismatch ? (
|
||||
<span className="text-xs text-yellow-400">(Color mismatch)</span>
|
||||
<span className="text-xs text-yellow-700 dark:text-yellow-400">(Color mismatch)</span>
|
||||
) : (
|
||||
<span className="text-xs text-bambu-green">(Ready)</span>
|
||||
)}
|
||||
@@ -264,8 +264,8 @@ export function FilamentMapping({
|
||||
item.status === 'match'
|
||||
? 'border-bambu-green/50 text-bambu-green'
|
||||
: item.status === 'type_only'
|
||||
? 'border-yellow-400/50 text-yellow-400'
|
||||
: 'border-orange-400/50 text-orange-400'
|
||||
? 'border-yellow-500 dark:border-yellow-400/50 text-yellow-700 dark:text-yellow-400'
|
||||
: 'border-orange-500 dark:border-orange-400/50 text-orange-700 dark:text-orange-400'
|
||||
} ${item.isManual ? 'ring-1 ring-blue-400/50' : ''}`}
|
||||
title={item.isManual ? 'Manually selected' : 'Auto-matched'}
|
||||
>
|
||||
@@ -315,11 +315,11 @@ export function FilamentMapping({
|
||||
<Check className="w-3 h-3 text-bambu-green" />
|
||||
) : item.status === 'type_only' ? (
|
||||
<span title="Same type, different color">
|
||||
<AlertTriangle className="w-3 h-3 text-yellow-400" />
|
||||
<AlertTriangle className="w-3 h-3 text-yellow-600 dark:text-yellow-400" />
|
||||
</span>
|
||||
) : (
|
||||
<span title="Filament type not loaded">
|
||||
<AlertTriangle className="w-3 h-3 text-orange-400" />
|
||||
<AlertTriangle className="w-3 h-3 text-orange-600 dark:text-orange-400" />
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
@@ -346,7 +346,7 @@ export function FilamentMapping({
|
||||
</span>
|
||||
</div>
|
||||
{hasTypeMismatch && (
|
||||
<p className="text-xs text-orange-400 mt-2">Required filament type not found in printer.</p>
|
||||
<p className="text-xs text-orange-700 dark:text-orange-400 mt-2">Required filament type not found in printer.</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -118,7 +118,7 @@ export function FilamentOverride({
|
||||
disabled={compatible.length === 0}
|
||||
className={`flex-1 px-2 py-1 rounded border text-xs bg-bambu-dark-secondary focus:outline-none focus:ring-1 focus:ring-bambu-green ${
|
||||
isOverridden
|
||||
? 'border-blue-400/50 text-blue-400'
|
||||
? 'border-blue-500/50 dark:border-blue-400/50 text-blue-700 dark:text-blue-400'
|
||||
: 'border-bambu-gray/30 text-bambu-gray'
|
||||
}`}
|
||||
>
|
||||
|
||||
@@ -35,7 +35,7 @@ export function PlateSelector({
|
||||
<Layers className="w-4 h-4 text-bambu-gray" />
|
||||
<span className="text-sm text-bambu-gray">Select Plate{multiSelect ? 's' : ''} to Print</span>
|
||||
{selectedPlates.size === 0 && (
|
||||
<span className="text-xs text-orange-400 flex items-center gap-1">
|
||||
<span className="text-xs text-orange-700 dark:text-orange-400 flex items-center gap-1">
|
||||
<AlertTriangle className="w-3 h-3" />
|
||||
Selection required
|
||||
</span>
|
||||
|
||||
@@ -109,7 +109,7 @@ export function PrintOptionsPanel({
|
||||
bypasses the per-filament-type derivation. */}
|
||||
<div className="pt-2 mt-1 border-t border-bambu-dark-tertiary/60">
|
||||
<div className="flex items-center gap-2 mb-1.5">
|
||||
<Flame className="w-3.5 h-3.5 text-amber-400" />
|
||||
<Flame className="w-3.5 h-3.5 text-amber-600 dark:text-amber-400" />
|
||||
<span className="text-sm text-white">{t('settings.preheatTitle', 'Preheat & Heat Soak')}</span>
|
||||
</div>
|
||||
<p className="text-xs text-bambu-gray mb-2">
|
||||
|
||||
@@ -171,8 +171,8 @@ function InlineMappingEditor({
|
||||
status === 'match'
|
||||
? 'border-bambu-green/50 text-bambu-green'
|
||||
: status === 'type_only'
|
||||
? 'border-yellow-400/50 text-yellow-400'
|
||||
: 'border-orange-400/50 text-orange-400'
|
||||
? 'border-yellow-500 dark:border-yellow-400/50 text-yellow-700 dark:text-yellow-400'
|
||||
: 'border-orange-500 dark:border-orange-400/50 text-orange-700 dark:text-orange-400'
|
||||
} ${isManual ? 'ring-1 ring-blue-400/50' : ''}`}
|
||||
title={isManual ? 'Manually selected' : 'Auto-matched'}
|
||||
>
|
||||
@@ -190,11 +190,11 @@ function InlineMappingEditor({
|
||||
<Check className="w-3 h-3 text-bambu-green" />
|
||||
) : status === 'type_only' ? (
|
||||
<span title="Same type, different color">
|
||||
<AlertTriangle className="w-3 h-3 text-yellow-400" />
|
||||
<AlertTriangle className="w-3 h-3 text-yellow-600 dark:text-yellow-400" />
|
||||
</span>
|
||||
) : (
|
||||
<span title="Filament type not loaded">
|
||||
<AlertTriangle className="w-3 h-3 text-orange-400" />
|
||||
<AlertTriangle className="w-3 h-3 text-orange-600 dark:text-orange-400" />
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
@@ -330,7 +330,7 @@ export function PrinterSelector({
|
||||
|
||||
if (displayPrinters.length === 0) {
|
||||
return (
|
||||
<div className="flex items-center gap-2 text-red-400 text-sm mb-4">
|
||||
<div className="flex items-center gap-2 text-red-700 dark:text-red-400 text-sm mb-4">
|
||||
<AlertCircle className="w-4 h-4" />
|
||||
No {showInactive ? '' : 'active '}printers available
|
||||
</div>
|
||||
@@ -562,7 +562,7 @@ export function PrinterSelector({
|
||||
{stateLabel && (
|
||||
<span className={`text-xs px-2 py-0.5 rounded-full ${
|
||||
busy
|
||||
? 'bg-yellow-500/20 text-yellow-400'
|
||||
? 'bg-yellow-100 dark:bg-yellow-500/20 text-yellow-700 dark:text-yellow-400'
|
||||
: 'bg-bambu-green/20 text-bambu-green'
|
||||
}`}>
|
||||
{stateLabel}
|
||||
@@ -606,8 +606,8 @@ export function PrinterSelector({
|
||||
mappingResult.matchStatus === 'full'
|
||||
? 'text-bambu-green'
|
||||
: mappingResult.matchStatus === 'partial'
|
||||
? 'text-yellow-400'
|
||||
: 'text-orange-400'
|
||||
? 'text-yellow-700 dark:text-yellow-400'
|
||||
: 'text-orange-700 dark:text-orange-400'
|
||||
}`}>
|
||||
({mappingResult.exactMatches}/{mappingResult.totalSlots} matched)
|
||||
</span>
|
||||
@@ -654,7 +654,7 @@ export function PrinterSelector({
|
||||
onClick={() => setShowAllPrinters(true)}
|
||||
className="text-xs text-bambu-gray hover:text-white transition-colors mt-2 flex items-center gap-1"
|
||||
>
|
||||
<AlertTriangle className="w-3 h-3 text-yellow-400" />
|
||||
<AlertTriangle className="w-3 h-3 text-yellow-600 dark:text-yellow-400" />
|
||||
{hiddenPrinterCount} other printer{hiddenPrinterCount > 1 ? 's' : ''} hidden (different model) —
|
||||
<span className="underline">show all</span>
|
||||
</button>
|
||||
@@ -673,7 +673,7 @@ export function PrinterSelector({
|
||||
|
||||
{/* Warning when no printer selected (only in printer mode) */}
|
||||
{assignmentMode === 'printer' && selectedCount === 0 && (
|
||||
<p className="text-xs text-orange-400 mt-1 flex items-center gap-1">
|
||||
<p className="text-xs text-orange-700 dark:text-orange-400 mt-1 flex items-center gap-1">
|
||||
<AlertCircle className="w-3 h-3" />
|
||||
Select at least one printer
|
||||
</p>
|
||||
@@ -681,7 +681,7 @@ export function PrinterSelector({
|
||||
|
||||
{/* Warning when no model selected (only in model mode) */}
|
||||
{assignmentMode === 'model' && !targetModel && (
|
||||
<p className="text-xs text-orange-400 mt-1 flex items-center gap-1">
|
||||
<p className="text-xs text-orange-700 dark:text-orange-400 mt-1 flex items-center gap-1">
|
||||
<AlertCircle className="w-3 h-3" />
|
||||
Select a target printer model
|
||||
</p>
|
||||
|
||||
@@ -219,7 +219,7 @@ export function ScheduleOptionsPanel({
|
||||
</div>
|
||||
</div>
|
||||
{(!isDateValid || !isTimeValid) && (
|
||||
<p className="mt-1 text-xs text-red-400">
|
||||
<p className="mt-1 text-xs text-red-700 dark:text-red-400">
|
||||
{t('printModal.invalidDateTime')}
|
||||
</p>
|
||||
)}
|
||||
|
||||
@@ -1127,9 +1127,9 @@ export function PrintModal({
|
||||
const selectedPrinter = printers?.find(p => p.id === selectedPrinters[0]);
|
||||
if (selectedPrinter && selectedPrinter.model && slicedForModel !== selectedPrinter.model) {
|
||||
return (
|
||||
<div className="p-3 mb-2 bg-yellow-500/10 border border-yellow-500/30 rounded-lg flex items-center gap-2">
|
||||
<AlertTriangle className="w-4 h-4 text-yellow-400 flex-shrink-0" />
|
||||
<span className="text-sm text-yellow-400">
|
||||
<div className="p-3 mb-2 bg-yellow-50 dark:bg-yellow-500/10 border border-yellow-300 dark:border-yellow-500/30 rounded-lg flex items-center gap-2">
|
||||
<AlertTriangle className="w-4 h-4 text-yellow-600 dark:text-yellow-400 flex-shrink-0" />
|
||||
<span className="text-sm text-yellow-700 dark:text-yellow-400">
|
||||
File was sliced for {slicedForModel}, but printing on {selectedPrinter.model}
|
||||
</span>
|
||||
</div>
|
||||
@@ -1140,9 +1140,9 @@ export function PrintModal({
|
||||
|
||||
{/* Warning when archive data couldn't be loaded */}
|
||||
{archiveDataMissing && (
|
||||
<div className="flex items-start gap-2 p-3 mb-2 bg-orange-500/10 border border-orange-500/30 rounded-lg text-sm">
|
||||
<AlertCircle className="w-4 h-4 text-orange-400 mt-0.5 flex-shrink-0" />
|
||||
<p className="text-orange-400">
|
||||
<div className="flex items-start gap-2 p-3 mb-2 bg-orange-50 dark:bg-orange-500/10 border border-orange-300 dark:border-orange-500/30 rounded-lg text-sm">
|
||||
<AlertCircle className="w-4 h-4 text-orange-600 dark:text-orange-400 mt-0.5 flex-shrink-0" />
|
||||
<p className="text-orange-700 dark:text-orange-400">
|
||||
Archive data unavailable. The source file may have been deleted. Filament mapping is disabled.
|
||||
</p>
|
||||
</div>
|
||||
@@ -1212,7 +1212,7 @@ export function PrintModal({
|
||||
|
||||
{/* Error message */}
|
||||
{updateQueueMutation.isError && (
|
||||
<div className="mb-4 p-3 bg-red-500/20 border border-red-500/50 rounded-lg text-sm text-red-400">
|
||||
<div className="mb-4 p-3 bg-red-100 dark:bg-red-500/20 border border-red-500/50 rounded-lg text-sm text-red-700 dark:text-red-400">
|
||||
{(updateQueueMutation.error as Error)?.message || 'Failed to complete operation'}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -86,7 +86,7 @@ export function PrinterInfoModal({ printer, status, totalPrintHours, onClose }:
|
||||
<span className={`inline-flex items-center gap-1.5 px-2 py-0.5 rounded-full text-xs font-medium ${
|
||||
status?.connected
|
||||
? 'bg-bambu-green/20 text-bambu-green'
|
||||
: 'bg-red-500/20 text-red-400'
|
||||
: 'bg-red-100 dark:bg-red-500/20 text-red-700 dark:text-red-400'
|
||||
}`}>
|
||||
<span className={`w-1.5 h-1.5 rounded-full ${status?.connected ? 'bg-bambu-green' : 'bg-red-400'}`} />
|
||||
{status?.connected ? t('printers.status.available') : t('printers.status.offline')}
|
||||
|
||||
@@ -48,7 +48,7 @@ export function PrinterQueueWidget({ printerId, printerModel, loadedFilamentType
|
||||
>
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="flex items-center gap-3 min-w-0 flex-1">
|
||||
<Calendar className="w-5 h-5 text-yellow-400 flex-shrink-0" />
|
||||
<Calendar className="w-5 h-5 text-yellow-600 dark:text-yellow-400 flex-shrink-0" />
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="text-xs text-bambu-gray">{t('queue.nextInQueue')}</p>
|
||||
<p className="text-sm text-white truncate">
|
||||
@@ -62,7 +62,7 @@ export function PrinterQueueWidget({ printerId, printerModel, loadedFilamentType
|
||||
{nextItem?.scheduled_time ? formatRelativeTime(nextItem.scheduled_time, 'system', t) : t('time.waiting')}
|
||||
</span>
|
||||
{totalPending > 1 && (
|
||||
<span className="text-xs px-1.5 py-0.5 bg-yellow-400/20 text-yellow-400 rounded">
|
||||
<span className="text-xs px-1.5 py-0.5 bg-yellow-100 dark:bg-yellow-400/20 text-yellow-700 dark:text-yellow-400 rounded">
|
||||
+{totalPending - 1}
|
||||
</span>
|
||||
)}
|
||||
|
||||
@@ -174,7 +174,7 @@ export function ProjectPageModal({ archiveId, archiveName, onClose }: ProjectPag
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<div className="text-red-400 text-center py-12">
|
||||
<div className="text-red-700 dark:text-red-400 text-center py-12">
|
||||
Failed to load project page data
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -22,10 +22,10 @@ export function QueueStatsBar({
|
||||
t: (key: string) => string;
|
||||
}) {
|
||||
const stats = [
|
||||
{ icon: Play, value: activeCount, label: t('queue.summary.printing'), color: 'text-blue-400' },
|
||||
{ icon: Clock, value: pendingCount, label: t('queue.summary.queued'), color: 'text-yellow-400' },
|
||||
{ icon: Play, value: activeCount, label: t('queue.summary.printing'), color: 'text-blue-600 dark:text-blue-400' },
|
||||
{ icon: Clock, value: pendingCount, label: t('queue.summary.queued'), color: 'text-yellow-600 dark:text-yellow-400' },
|
||||
{ icon: Timer, value: formatDuration(totalTime), label: t('queue.summary.totalTime'), color: 'text-bambu-green' },
|
||||
{ icon: Weight, value: formatWeight(totalWeight), label: t('queue.summary.totalWeight'), color: 'text-purple-400' },
|
||||
{ icon: Weight, value: formatWeight(totalWeight), label: t('queue.summary.totalWeight'), color: 'text-purple-600 dark:text-purple-400' },
|
||||
{ icon: CheckCircle, value: historyCount, label: t('queue.summary.history'), color: 'text-bambu-gray' },
|
||||
];
|
||||
|
||||
|
||||
@@ -353,9 +353,9 @@ export function QueueTimelineView({
|
||||
<div className="w-32 sm:w-40 shrink-0 px-3 py-3 border-r border-bambu-dark-tertiary flex items-center gap-2">
|
||||
<PrinterIcon className={`w-3.5 h-3.5 shrink-0 ${
|
||||
lane.printerId == null && lane.targetModel == null
|
||||
? 'text-orange-400'
|
||||
? 'text-orange-600 dark:text-orange-400'
|
||||
: lane.targetModel
|
||||
? 'text-blue-400'
|
||||
? 'text-blue-600 dark:text-blue-400'
|
||||
: 'text-bambu-green'
|
||||
}`} />
|
||||
<span className="text-sm text-white truncate">{lane.label}</span>
|
||||
|
||||
@@ -212,7 +212,7 @@ export function RestoreModal({ onClose, onRestore, onSuccess }: RestoreModalProp
|
||||
<div>
|
||||
<p className="text-white font-medium flex items-center gap-2">
|
||||
{overwrite ? (
|
||||
<RefreshCw className="w-4 h-4 text-orange-400" />
|
||||
<RefreshCw className="w-4 h-4 text-orange-600 dark:text-orange-400" />
|
||||
) : (
|
||||
<SkipForward className="w-4 h-4 text-bambu-gray" />
|
||||
)}
|
||||
@@ -361,8 +361,8 @@ export function RestoreModal({ onClose, onRestore, onSuccess }: RestoreModalProp
|
||||
<AlertTriangle className="w-4 h-4 text-orange-500" />
|
||||
{t('backup.newApiKeysGenerated')}
|
||||
</h4>
|
||||
<div className="p-3 rounded bg-orange-500/10 border border-orange-500/30">
|
||||
<p className="text-xs text-orange-200 mb-2">
|
||||
<div className="p-3 rounded bg-orange-50 dark:bg-orange-500/10 border border-orange-300 dark:border-orange-500/30">
|
||||
<p className="text-xs text-orange-800 dark:text-orange-200 mb-2">
|
||||
{t('backup.keysShownOnce')}
|
||||
</p>
|
||||
<div className="space-y-2">
|
||||
|
||||
@@ -266,7 +266,7 @@ function PickStep({
|
||||
) : targetName ? (
|
||||
<span>{targetName}</span>
|
||||
) : (
|
||||
<span className="text-amber-400">
|
||||
<span className="text-amber-700 dark:text-amber-400">
|
||||
{t('library.runWithPipeline.noTarget', 'No target printer set')}
|
||||
</span>
|
||||
)}
|
||||
@@ -323,7 +323,7 @@ function ConfirmStep({
|
||||
<ul className="space-y-1.5">
|
||||
{report.issues.map((issue, idx) => (
|
||||
<li key={idx} className="flex items-start gap-2 text-xs">
|
||||
<AlertTriangle className="w-3.5 h-3.5 text-amber-400 flex-shrink-0 mt-0.5" />
|
||||
<AlertTriangle className="w-3.5 h-3.5 text-amber-600 dark:text-amber-400 flex-shrink-0 mt-0.5" />
|
||||
<span className="text-bambu-gray">
|
||||
<IssueText issue={issue} />
|
||||
</span>
|
||||
|
||||
@@ -80,14 +80,14 @@ export function SecurityStatusCard() {
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-red-400" data-testid="encryption-error">{t('common.errorLoading')}</div>
|
||||
<div className="text-red-700 dark:text-red-400" data-testid="encryption-error">{t('common.errorLoading')}</div>
|
||||
{/* S5: manual recovery button — the bounded auto-retry above stops
|
||||
after 3 consecutive failures so the operator needs an explicit
|
||||
way to reset polling without reloading the whole page. */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => refetch()}
|
||||
className="mt-2 text-sm text-blue-400 underline hover:text-blue-300"
|
||||
className="mt-2 text-sm text-blue-700 dark:text-blue-400 underline hover:text-blue-800 dark:hover:text-blue-300"
|
||||
data-testid="encryption-retry-button"
|
||||
>
|
||||
{t('common.retry')}
|
||||
@@ -109,23 +109,23 @@ export function SecurityStatusCard() {
|
||||
let statusBody: string;
|
||||
|
||||
if (data.decryption_broken) {
|
||||
severityClasses = 'bg-red-500/20 border-red-500/50 text-red-400';
|
||||
icon = <XCircle className="text-red-400" size={20} />;
|
||||
severityClasses = 'bg-red-100 dark:bg-red-500/20 border-red-400 dark:border-red-500/50 text-red-700 dark:text-red-400';
|
||||
icon = <XCircle className="text-red-600 dark:text-red-400" size={20} />;
|
||||
statusLabel = t('settings.encryption.decryptionBrokenTitle');
|
||||
statusBody = t('settings.encryption.decryptionBrokenError', { count: totalEncrypted });
|
||||
} else if (data.key_source === 'generated') {
|
||||
severityClasses = 'bg-amber-500/10 border-amber-500/30 text-amber-400';
|
||||
icon = <ShieldCheck className="text-amber-400" size={20} />;
|
||||
severityClasses = 'bg-amber-50 dark:bg-amber-500/10 border-amber-300 dark:border-amber-500/30 text-amber-700 dark:text-amber-400';
|
||||
icon = <ShieldCheck className="text-amber-600 dark:text-amber-400" size={20} />;
|
||||
statusLabel = t('settings.encryption.enabledGenerated');
|
||||
statusBody = t('settings.encryption.backupHint');
|
||||
} else if (totalLegacy > 0) {
|
||||
severityClasses = 'bg-amber-500/10 border-amber-500/30 text-amber-400';
|
||||
icon = <AlertTriangle className="text-amber-400" size={20} />;
|
||||
severityClasses = 'bg-amber-50 dark:bg-amber-500/10 border-amber-300 dark:border-amber-500/30 text-amber-700 dark:text-amber-400';
|
||||
icon = <AlertTriangle className="text-amber-600 dark:text-amber-400" size={20} />;
|
||||
statusLabel = data.key_source === 'env' ? t('settings.encryption.enabledFromEnv') : t('settings.encryption.enabledFromFile');
|
||||
statusBody = t('settings.encryption.legacyRowsWarning', { count: totalLegacy });
|
||||
} else if (data.key_configured) {
|
||||
severityClasses = 'bg-green-500/20 border-green-500/30 text-green-400';
|
||||
icon = <ShieldCheck className="text-green-400" size={20} />;
|
||||
severityClasses = 'bg-green-100 dark:bg-green-500/20 border-green-300 dark:border-green-500/30 text-green-700 dark:text-green-400';
|
||||
icon = <ShieldCheck className="text-green-600 dark:text-green-400" size={20} />;
|
||||
statusLabel = data.key_source === 'env' ? t('settings.encryption.enabledFromEnv') : t('settings.encryption.enabledFromFile');
|
||||
statusBody = t('settings.encryption.allEncrypted');
|
||||
} else {
|
||||
@@ -157,7 +157,7 @@ export function SecurityStatusCard() {
|
||||
</div>
|
||||
{showConcurrentLegacyWarning && (
|
||||
<div
|
||||
className="mt-2 p-3 border rounded-lg bg-amber-500/10 border-amber-500/30 text-amber-400"
|
||||
className="mt-2 p-3 border rounded-lg bg-amber-50 dark:bg-amber-500/10 border-amber-300 dark:border-amber-500/30 text-amber-700 dark:text-amber-400"
|
||||
data-testid="encryption-legacy-warning"
|
||||
>
|
||||
<p className="text-sm">{t('settings.encryption.legacyRowsWarning', { count: totalLegacy })}</p>
|
||||
@@ -165,7 +165,7 @@ export function SecurityStatusCard() {
|
||||
)}
|
||||
{data.migration_error_count > 0 && (
|
||||
<div
|
||||
className="mt-2 p-3 border rounded-lg bg-amber-500/10 border-amber-500/30 text-amber-400"
|
||||
className="mt-2 p-3 border rounded-lg bg-amber-50 dark:bg-amber-500/10 border-amber-300 dark:border-amber-500/30 text-amber-700 dark:text-amber-400"
|
||||
data-testid="encryption-migration-warning"
|
||||
>
|
||||
<p className="text-sm">
|
||||
|
||||
@@ -240,7 +240,7 @@ export function SkipObjectsModal({ printerId, isOpen, onClose }: SkipObjectsModa
|
||||
<span className={`text-lg font-mono font-bold ${obj.skipped ? 'text-red-500 dark:text-red-400' : 'text-green-600 dark:text-bambu-green'}`}>
|
||||
{obj.id}
|
||||
</span>
|
||||
<span className={`text-[8px] uppercase tracking-wider ${obj.skipped ? 'text-red-400/60' : 'text-green-500/60 dark:text-bambu-green/60'}`}>
|
||||
<span className={`text-[8px] uppercase tracking-wider ${obj.skipped ? 'text-red-700/70 dark:text-red-400/60' : 'text-green-500/60 dark:text-bambu-green/60'}`}>
|
||||
ID
|
||||
</span>
|
||||
</div>
|
||||
@@ -251,7 +251,7 @@ export function SkipObjectsModal({ printerId, isOpen, onClose }: SkipObjectsModa
|
||||
{obj.name}
|
||||
</span>
|
||||
{obj.skipped && (
|
||||
<span className="text-[10px] text-red-400/60">{t('printers.willBeSkipped')}</span>
|
||||
<span className="text-[10px] text-red-700/70 dark:text-red-400/60">{t('printers.willBeSkipped')}</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -557,7 +557,7 @@ export function SliceModal({ source, onClose }: SliceModalProps) {
|
||||
)}
|
||||
|
||||
{presetsQuery.isError && (
|
||||
<div className="text-sm text-red-400" role="alert">
|
||||
<div className="text-sm text-red-700 dark:text-red-400" role="alert">
|
||||
{t(
|
||||
'slice.presetsLoadFailed',
|
||||
'Failed to load presets. Open Settings → Profiles to import them, or sign in to Bambu Cloud.',
|
||||
@@ -787,7 +787,7 @@ export function SliceModal({ source, onClose }: SliceModalProps) {
|
||||
)}
|
||||
|
||||
{errorMessage && (
|
||||
<div className="text-sm text-red-400 bg-red-900/20 border border-red-900/40 rounded p-2" role="alert">
|
||||
<div className="text-sm text-red-700 dark:text-red-400 bg-red-900/20 border border-red-900/40 rounded p-2" role="alert">
|
||||
{errorMessage}
|
||||
</div>
|
||||
)}
|
||||
@@ -892,7 +892,7 @@ function CloudStatusBanner({
|
||||
|
||||
const tones: Record<'expired' | 'unreachable', { tone: string; icon: typeof Cloud }> = {
|
||||
expired: {
|
||||
tone: 'border-amber-700/40 bg-amber-900/20 text-amber-200',
|
||||
tone: 'border-amber-300 dark:border-amber-700/40 bg-amber-50 dark:bg-amber-900/20 text-amber-800 dark:text-amber-200',
|
||||
icon: CloudOff,
|
||||
},
|
||||
unreachable: {
|
||||
|
||||
@@ -180,7 +180,7 @@ export function SlicerPipelinesPanel() {
|
||||
</div>
|
||||
)}
|
||||
{error && (
|
||||
<div className="text-sm text-red-400">
|
||||
<div className="text-sm text-red-700 dark:text-red-400">
|
||||
{t('settings.pipelines.loadError', 'Could not load pipelines.')}
|
||||
</div>
|
||||
)}
|
||||
@@ -532,7 +532,7 @@ function PipelineRow({
|
||||
<span
|
||||
className={`text-xs px-1.5 py-0.5 rounded inline-flex items-center gap-1 ${
|
||||
needsTarget
|
||||
? 'bg-amber-500/15 text-amber-400'
|
||||
? 'bg-amber-100 dark:bg-amber-500/15 text-amber-700 dark:text-amber-400'
|
||||
: 'bg-bambu-dark-tertiary text-bambu-gray'
|
||||
}`}
|
||||
>
|
||||
@@ -597,7 +597,7 @@ function PipelineRow({
|
||||
onClick={onDelete}
|
||||
disabled={deleting}
|
||||
aria-label={t('settings.pipelines.action.delete', 'Delete')}
|
||||
className="p-1.5 text-bambu-gray hover:text-red-400 hover:bg-bambu-dark-tertiary rounded disabled:opacity-50"
|
||||
className="p-1.5 text-bambu-gray hover:text-red-600 dark:hover:text-red-400 hover:bg-bambu-dark-tertiary rounded disabled:opacity-50"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</button>
|
||||
@@ -689,7 +689,7 @@ function PipelineRow({
|
||||
)}
|
||||
|
||||
{needsTarget && !editing && (
|
||||
<div className="mt-2 flex items-center gap-1.5 text-xs text-amber-400">
|
||||
<div className="mt-2 flex items-center gap-1.5 text-xs text-amber-700 dark:text-amber-400">
|
||||
<AlertTriangle className="w-3.5 h-3.5" />
|
||||
{t(
|
||||
'settings.pipelines.noTargetWarning',
|
||||
@@ -699,7 +699,7 @@ function PipelineRow({
|
||||
)}
|
||||
|
||||
{hasStaleRef && !editing && (
|
||||
<div className="mt-2 flex items-center gap-1.5 text-xs text-amber-400">
|
||||
<div className="mt-2 flex items-center gap-1.5 text-xs text-amber-700 dark:text-amber-400">
|
||||
<AlertTriangle className="w-3.5 h-3.5" />
|
||||
{t(
|
||||
'settings.pipelines.staleWarning',
|
||||
@@ -715,12 +715,12 @@ function RunStatusBadge({ status }: { status: PipelineRun['status'] }) {
|
||||
const { t } = useTranslation();
|
||||
const colourClass: Record<PipelineRun['status'], string> = {
|
||||
queued: 'text-bambu-gray',
|
||||
slicing: 'text-blue-400',
|
||||
dispatching: 'text-blue-400',
|
||||
slicing: 'text-blue-700 dark:text-blue-400',
|
||||
dispatching: 'text-blue-700 dark:text-blue-400',
|
||||
in_progress: 'text-bambu-green',
|
||||
completed: 'text-bambu-green',
|
||||
failed: 'text-red-400',
|
||||
partial_failure: 'text-amber-400',
|
||||
failed: 'text-red-700 dark:text-red-400',
|
||||
partial_failure: 'text-amber-700 dark:text-amber-400',
|
||||
cancelled: 'text-bambu-gray',
|
||||
};
|
||||
return (
|
||||
@@ -745,7 +745,7 @@ function PresetLine({
|
||||
{name ? (
|
||||
<span className="text-white">{name}</span>
|
||||
) : (
|
||||
<span className="text-amber-400">[{SOURCE_LABEL[ref.source]} #{ref.id}]</span>
|
||||
<span className="text-amber-700 dark:text-amber-400">[{SOURCE_LABEL[ref.source]} #{ref.id}]</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -126,17 +126,17 @@ export function SmartPlugCard({ plug, onEdit }: SmartPlugCardProps) {
|
||||
<div className="flex items-center gap-3 min-w-0 flex-1">
|
||||
<div className={`p-2 rounded-lg flex-shrink-0 ${
|
||||
plug.plug_type === 'mqtt'
|
||||
? (isReachable ? 'bg-teal-500/20' : 'bg-red-500/20')
|
||||
: (isReachable ? (isOn ? 'bg-bambu-green/20' : 'bg-bambu-dark') : 'bg-red-500/20')
|
||||
? (isReachable ? 'bg-teal-100 dark:bg-teal-500/20' : 'bg-red-100 dark:bg-red-500/20')
|
||||
: (isReachable ? (isOn ? 'bg-bambu-green/20' : 'bg-bambu-dark') : 'bg-red-100 dark:bg-red-500/20')
|
||||
}`}>
|
||||
{plug.plug_type === 'mqtt' ? (
|
||||
<Radio className={`w-5 h-5 ${isReachable ? 'text-teal-400' : 'text-red-400'}`} />
|
||||
<Radio className={`w-5 h-5 ${isReachable ? 'text-teal-600 dark:text-teal-400' : 'text-red-600 dark:text-red-400'}`} />
|
||||
) : plug.plug_type === 'homeassistant' ? (
|
||||
<Home className={`w-5 h-5 ${isReachable ? (isOn ? 'text-bambu-green' : 'text-bambu-gray') : 'text-red-400'}`} />
|
||||
<Home className={`w-5 h-5 ${isReachable ? (isOn ? 'text-bambu-green' : 'text-bambu-gray') : 'text-red-600 dark:text-red-400'}`} />
|
||||
) : plug.plug_type === 'rest' ? (
|
||||
<Globe className={`w-5 h-5 ${isReachable ? (isOn ? 'text-bambu-green' : 'text-bambu-gray') : 'text-red-400'}`} />
|
||||
<Globe className={`w-5 h-5 ${isReachable ? (isOn ? 'text-bambu-green' : 'text-bambu-gray') : 'text-red-600 dark:text-red-400'}`} />
|
||||
) : (
|
||||
<Plug className={`w-5 h-5 ${isReachable ? (isOn ? 'text-bambu-green' : 'text-bambu-gray') : 'text-red-400'}`} />
|
||||
<Plug className={`w-5 h-5 ${isReachable ? (isOn ? 'text-bambu-green' : 'text-bambu-gray') : 'text-red-600 dark:text-red-400'}`} />
|
||||
)}
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
@@ -157,19 +157,19 @@ export function SmartPlugCard({ plug, onEdit }: SmartPlugCardProps) {
|
||||
) : plug.plug_type === 'mqtt' ? (
|
||||
/* MQTT plugs - show badge and checkmark when receiving data */
|
||||
<div className="flex items-center gap-1.5 text-sm whitespace-nowrap">
|
||||
<span className="px-1.5 py-0.5 bg-teal-500/20 text-teal-400 text-[10px] font-medium rounded flex-shrink-0">MQTT</span>
|
||||
<span className="px-1.5 py-0.5 bg-teal-100 dark:bg-teal-500/20 text-teal-700 dark:text-teal-400 text-[10px] font-medium rounded flex-shrink-0">MQTT</span>
|
||||
{isReachable && <span className="text-status-ok">✓</span>}
|
||||
</div>
|
||||
) : plug.plug_type === 'homeassistant' ? (
|
||||
<div className="flex items-center gap-1 text-sm">
|
||||
<span className="px-1 py-0.5 bg-blue-500/20 text-blue-400 text-[10px] font-medium rounded">HA</span>
|
||||
<span className="px-1 py-0.5 bg-blue-100 dark:bg-blue-500/20 text-blue-700 dark:text-blue-400 text-[10px] font-medium rounded">HA</span>
|
||||
<span className={isReachable ? (isOn ? 'text-status-ok' : 'text-bambu-gray') : 'text-status-error'}>
|
||||
{isReachable ? (status?.state || '?') : t('smartPlugs.offline')}
|
||||
</span>
|
||||
</div>
|
||||
) : plug.plug_type === 'rest' ? (
|
||||
<div className="flex items-center gap-1 text-sm">
|
||||
<span className="px-1 py-0.5 bg-purple-500/20 text-purple-400 text-[10px] font-medium rounded">REST</span>
|
||||
<span className="px-1 py-0.5 bg-purple-100 dark:bg-purple-500/20 text-purple-700 dark:text-purple-400 text-[10px] font-medium rounded">REST</span>
|
||||
<span className={isReachable ? (isOn ? 'text-status-ok' : 'text-bambu-gray') : 'text-status-error'}>
|
||||
{isReachable ? (status?.state || '?') : t('smartPlugs.offline')}
|
||||
</span>
|
||||
@@ -214,19 +214,19 @@ export function SmartPlugCard({ plug, onEdit }: SmartPlugCardProps) {
|
||||
{(plug.power_alert_enabled || plug.schedule_enabled || plug.plug_type === 'mqtt') && (
|
||||
<div className="flex flex-wrap gap-1.5 mb-3">
|
||||
{plug.plug_type === 'mqtt' && (
|
||||
<span className="flex items-center gap-1 px-2 py-0.5 bg-teal-500/20 text-teal-400 text-xs rounded-full">
|
||||
<span className="flex items-center gap-1 px-2 py-0.5 bg-teal-100 dark:bg-teal-500/20 text-teal-700 dark:text-teal-400 text-xs rounded-full">
|
||||
<Eye className="w-3 h-3" />
|
||||
{t('smartPlugs.monitorOnly')}
|
||||
</span>
|
||||
)}
|
||||
{plug.power_alert_enabled && (
|
||||
<span className="flex items-center gap-1 px-2 py-0.5 bg-yellow-500/20 text-yellow-400 text-xs rounded-full">
|
||||
<span className="flex items-center gap-1 px-2 py-0.5 bg-yellow-100 dark:bg-yellow-500/20 text-yellow-700 dark:text-yellow-400 text-xs rounded-full">
|
||||
<Bell className="w-3 h-3" />
|
||||
{t('smartPlugs.alerts')}
|
||||
</span>
|
||||
)}
|
||||
{plug.schedule_enabled && (
|
||||
<span className="flex items-center gap-1 px-2 py-0.5 bg-blue-500/20 text-blue-400 text-xs rounded-full">
|
||||
<span className="flex items-center gap-1 px-2 py-0.5 bg-blue-100 dark:bg-blue-500/20 text-blue-700 dark:text-blue-400 text-xs rounded-full">
|
||||
<Calendar className="w-3 h-3" />
|
||||
{plug.schedule_on_time && plug.schedule_off_time
|
||||
? `${plug.schedule_on_time} - ${plug.schedule_off_time}`
|
||||
@@ -503,7 +503,7 @@ export function SmartPlugCard({ plug, onEdit }: SmartPlugCardProps) {
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
onClick={() => setShowDeleteConfirm(true)}
|
||||
className="text-red-400 hover:text-red-300"
|
||||
className="text-red-600 dark:text-red-400 hover:text-red-700 dark:hover:text-red-300"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</Button>
|
||||
|
||||
@@ -255,8 +255,8 @@ export function SpoolCatalogSettings() {
|
||||
</div>
|
||||
|
||||
{selectedIds.size > 0 && (
|
||||
<div className="flex items-center gap-2 mt-2 px-3 py-2 bg-red-500/10 border border-red-500/30 rounded-lg">
|
||||
<span className="text-sm text-red-400">
|
||||
<div className="flex items-center gap-2 mt-2 px-3 py-2 bg-red-50 dark:bg-red-500/10 border border-red-300 dark:border-red-500/30 rounded-lg">
|
||||
<span className="text-sm text-red-700 dark:text-red-400">
|
||||
{t('settings.catalog.selectedCount', { count: selectedIds.size })}
|
||||
</span>
|
||||
<button
|
||||
|
||||
@@ -125,9 +125,9 @@ export function SpoolCsvImportModal({ onClose, onImported }: SpoolCsvImportModal
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<div className="p-3 bg-red-500/10 border border-red-500/30 rounded-lg flex items-start gap-3">
|
||||
<XCircle className="w-5 h-5 text-red-400 mt-0.5 flex-shrink-0" />
|
||||
<p className="text-sm text-red-300 break-words">{error}</p>
|
||||
<div className="p-3 bg-red-50 dark:bg-red-500/10 border border-red-300 dark:border-red-500/30 rounded-lg flex items-start gap-3">
|
||||
<XCircle className="w-5 h-5 text-red-600 dark:text-red-400 mt-0.5 flex-shrink-0" />
|
||||
<p className="text-sm text-red-700 dark:text-red-300 break-words">{error}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -135,10 +135,10 @@ export function SpoolCsvImportModal({ onClose, onImported }: SpoolCsvImportModal
|
||||
<>
|
||||
{/* Summary */}
|
||||
<div className="flex flex-wrap gap-3 text-sm">
|
||||
<span className="px-2 py-1 rounded bg-green-500/10 text-green-400">
|
||||
<span className="px-2 py-1 rounded bg-green-50 dark:bg-green-500/10 text-green-700 dark:text-green-400">
|
||||
{t('inventory.csv.validCount', '{{count}} valid', { count: preview.valid_count })}
|
||||
</span>
|
||||
<span className="px-2 py-1 rounded bg-red-500/10 text-red-400">
|
||||
<span className="px-2 py-1 rounded bg-red-50 dark:bg-red-500/10 text-red-700 dark:text-red-400">
|
||||
{t('inventory.csv.errorCount', '{{count}} error', { count: preview.error_count })}
|
||||
</span>
|
||||
<span className="px-2 py-1 rounded bg-bambu-dark text-bambu-gray">
|
||||
@@ -147,9 +147,9 @@ export function SpoolCsvImportModal({ onClose, onImported }: SpoolCsvImportModal
|
||||
</div>
|
||||
|
||||
{preview.warnings.length > 0 && (
|
||||
<div className="p-3 bg-yellow-500/10 border border-yellow-500/30 rounded-lg space-y-1">
|
||||
<div className="p-3 bg-yellow-50 dark:bg-yellow-500/10 border border-yellow-300 dark:border-yellow-500/30 rounded-lg space-y-1">
|
||||
{preview.warnings.map((w, i) => (
|
||||
<p key={i} className="text-xs text-yellow-300">{w}</p>
|
||||
<p key={i} className="text-xs text-yellow-700 dark:text-yellow-300">{w}</p>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
@@ -176,7 +176,7 @@ export function SpoolCsvImportModal({ onClose, onImported }: SpoolCsvImportModal
|
||||
<div className="flex items-center gap-1.5">
|
||||
{statusIcon(row.status)}
|
||||
{row.status === 'error' && row.reason && (
|
||||
<span className="text-xs text-red-400 break-words">{row.reason}</span>
|
||||
<span className="text-xs text-red-700 dark:text-red-400 break-words">{row.reason}</span>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
@@ -203,7 +203,7 @@ export function SpoolCsvImportModal({ onClose, onImported }: SpoolCsvImportModal
|
||||
)}
|
||||
{row.duplicate_of_existing && (
|
||||
<span title={t('inventory.csv.duplicateExisting', 'A spool with this material, brand and color already exists — it will still be imported as a new spool')}>
|
||||
<Copy className="w-3.5 h-3.5 text-amber-400 flex-shrink-0" />
|
||||
<Copy className="w-3.5 h-3.5 text-amber-600 dark:text-amber-400 flex-shrink-0" />
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -817,7 +817,7 @@ export function SpoolFormModal({
|
||||
{mode === 'create' && (
|
||||
<div className="flex items-center justify-between px-4 py-2 border-b border-bambu-dark-tertiary flex-shrink-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<Zap className="w-4 h-4 text-amber-400" />
|
||||
<Zap className="w-4 h-4 text-amber-600 dark:text-amber-400" />
|
||||
<span className="text-sm text-white">{t('inventory.quickAdd')}</span>
|
||||
</div>
|
||||
<button
|
||||
@@ -880,7 +880,7 @@ export function SpoolFormModal({
|
||||
{spoolmanMode && !isEditing && (
|
||||
<div>
|
||||
{filamentsError ? (
|
||||
<p className="text-sm text-red-400 px-1">{t('inventory.spoolmanCatalogLoadFailed')}</p>
|
||||
<p className="text-sm text-red-700 dark:text-red-400 px-1">{t('inventory.spoolmanCatalogLoadFailed')}</p>
|
||||
) : (
|
||||
<SpoolmanFilamentPicker
|
||||
filaments={spoolmanFilaments}
|
||||
|
||||
@@ -19,8 +19,8 @@ interface SpoolUsageHistoryProps {
|
||||
|
||||
const STATUS_COLORS: Record<string, string> = {
|
||||
completed: 'text-bambu-green',
|
||||
failed: 'text-red-400',
|
||||
aborted: 'text-yellow-400',
|
||||
failed: 'text-red-700 dark:text-red-400',
|
||||
aborted: 'text-yellow-700 dark:text-yellow-400',
|
||||
};
|
||||
|
||||
export function SpoolUsageHistory({ spoolId }: SpoolUsageHistoryProps) {
|
||||
@@ -67,7 +67,7 @@ export function SpoolUsageHistory({ spoolId }: SpoolUsageHistoryProps) {
|
||||
size="sm"
|
||||
onClick={() => clearMutation.mutate()}
|
||||
disabled={clearMutation.isPending}
|
||||
className="text-xs text-bambu-gray hover:text-red-400"
|
||||
className="text-xs text-bambu-gray hover:text-red-700 dark:hover:text-red-400"
|
||||
>
|
||||
<Trash2 className="w-3 h-3 mr-1" />
|
||||
{t('inventory.clearHistory')}
|
||||
|
||||
@@ -345,18 +345,18 @@ export function SpoolmanSettings() {
|
||||
{localEnabled && (
|
||||
<div className="space-y-4">
|
||||
{/* Info banner about sync requirements */}
|
||||
<div className="p-3 bg-blue-500/10 border border-blue-500/30 rounded-lg">
|
||||
<div className="p-3 bg-blue-50 dark:bg-blue-500/10 border border-blue-300 dark:border-blue-500/30 rounded-lg">
|
||||
<div className="flex gap-2">
|
||||
<Info className="w-4 h-4 text-blue-400 flex-shrink-0 mt-0.5" />
|
||||
<div className="text-xs text-blue-300">
|
||||
<Info className="w-4 h-4 text-blue-600 dark:text-blue-400 flex-shrink-0 mt-0.5" />
|
||||
<div className="text-xs text-blue-700 dark:text-blue-300">
|
||||
<p className="font-medium mb-1">{t('settings.howSyncWorks')}</p>
|
||||
<ul className="list-disc list-inside space-y-0.5 text-blue-300/80">
|
||||
<ul className="list-disc list-inside space-y-0.5 text-blue-700/90 dark:text-blue-300/80">
|
||||
<li>{t('settings.syncInfoRfidOnly')}</li>
|
||||
<li>{t('settings.syncInfoAutoCreate')}</li>
|
||||
<li>{t('settings.syncInfoThirdPartySkipped')}</li>
|
||||
</ul>
|
||||
<p className="font-medium mt-2 mb-1">{t('settings.linkingExistingSpools')}</p>
|
||||
<p className="text-blue-300/80">
|
||||
<p className="text-blue-700/90 dark:text-blue-300/80">
|
||||
{t('settings.linkingExistingSpoolsDesc')}
|
||||
</p>
|
||||
</div>
|
||||
@@ -495,7 +495,7 @@ export function SpoolmanSettings() {
|
||||
|
||||
{/* Error display */}
|
||||
{(connectMutation.isError || disconnectMutation.isError) && (
|
||||
<div className="mb-3 p-2 bg-red-500/20 border border-red-500/50 rounded text-sm text-red-400">
|
||||
<div className="mb-3 p-2 bg-red-100 dark:bg-red-500/20 border border-red-300 dark:border-red-500/50 rounded text-sm text-red-700 dark:text-red-400">
|
||||
{((connectMutation.error || disconnectMutation.error) as Error).message}
|
||||
</div>
|
||||
)}
|
||||
@@ -576,8 +576,8 @@ export function SpoolmanSettings() {
|
||||
<div
|
||||
className={`p-2 rounded text-sm ${
|
||||
syncResult.success
|
||||
? 'bg-green-500/20 border border-green-500/50 text-green-400'
|
||||
: 'bg-yellow-500/20 border border-yellow-500/50 text-yellow-400'
|
||||
? 'bg-green-100 dark:bg-green-500/20 border border-green-300 dark:border-green-500/50 text-green-700 dark:text-green-400'
|
||||
: 'bg-yellow-100 dark:bg-yellow-500/20 border border-yellow-300 dark:border-yellow-500/50 text-yellow-700 dark:text-yellow-400'
|
||||
}`}
|
||||
>
|
||||
{syncResult.success
|
||||
@@ -587,8 +587,8 @@ export function SpoolmanSettings() {
|
||||
|
||||
{/* Skipped spools */}
|
||||
{syncResult.skipped_count > 0 && (
|
||||
<div className="p-2 bg-amber-500/10 border border-amber-500/30 rounded text-sm">
|
||||
<div className="flex items-center justify-between text-amber-400 mb-1">
|
||||
<div className="p-2 bg-amber-50 dark:bg-amber-500/10 border border-amber-300 dark:border-amber-500/30 rounded text-sm">
|
||||
<div className="flex items-center justify-between text-amber-700 dark:text-amber-400 mb-1">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<AlertTriangle className="w-3.5 h-3.5" />
|
||||
<span className="font-medium">
|
||||
@@ -598,13 +598,13 @@ export function SpoolmanSettings() {
|
||||
{syncResult.skipped_count > 5 && (
|
||||
<button
|
||||
onClick={() => setShowAllSkipped(!showAllSkipped)}
|
||||
className="text-xs text-amber-400 hover:text-amber-300 underline"
|
||||
className="text-xs text-amber-700 dark:text-amber-400 hover:text-amber-800 dark:hover:text-amber-300 underline"
|
||||
>
|
||||
{showAllSkipped ? 'Show less' : 'Show all'}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<ul className="text-xs text-amber-300/80 space-y-0.5">
|
||||
<ul className="text-xs text-amber-700/90 dark:text-amber-300/80 space-y-0.5">
|
||||
{(showAllSkipped ? syncResult.skipped : syncResult.skipped.slice(0, 5)).map((s, i) => (
|
||||
<li key={i} className="flex items-center gap-2">
|
||||
{s.color && (
|
||||
@@ -614,11 +614,11 @@ export function SpoolmanSettings() {
|
||||
/>
|
||||
)}
|
||||
<span>{s.location}</span>
|
||||
<span className="text-amber-300/60">- {s.reason}</span>
|
||||
<span className="text-amber-700/70 dark:text-amber-300/60">- {s.reason}</span>
|
||||
</li>
|
||||
))}
|
||||
{!showAllSkipped && syncResult.skipped_count > 5 && (
|
||||
<li className="text-amber-300/60 italic">
|
||||
<li className="text-amber-700/70 dark:text-amber-300/60 italic">
|
||||
...and {syncResult.skipped_count - 5} more
|
||||
</li>
|
||||
)}
|
||||
@@ -628,9 +628,9 @@ export function SpoolmanSettings() {
|
||||
|
||||
{/* Errors */}
|
||||
{syncResult.errors.length > 0 && (
|
||||
<div className="p-2 bg-red-500/10 border border-red-500/30 rounded text-sm">
|
||||
<div className="text-red-400 font-medium mb-1">Errors:</div>
|
||||
<ul className="text-xs text-red-300/80 space-y-0.5">
|
||||
<div className="p-2 bg-red-50 dark:bg-red-500/10 border border-red-300 dark:border-red-500/30 rounded text-sm">
|
||||
<div className="text-red-700 dark:text-red-400 font-medium mb-1">Errors:</div>
|
||||
<ul className="text-xs text-red-700/90 dark:text-red-300/80 space-y-0.5">
|
||||
{syncResult.errors.map((err, i) => (
|
||||
<li key={i}>{err}</li>
|
||||
))}
|
||||
|
||||
@@ -53,9 +53,9 @@ function SwitchItem({ plug }: { plug: SmartPlug }) {
|
||||
: (isReachable ? (isOn ? 'bg-bambu-green/20' : 'bg-bambu-dark') : 'bg-red-500/20')
|
||||
}`}>
|
||||
{isMqtt ? (
|
||||
<Radio className={`w-4 h-4 ${isReachable ? 'text-teal-400' : 'text-red-400'}`} />
|
||||
<Radio className={`w-4 h-4 ${isReachable ? 'text-teal-600 dark:text-teal-400' : 'text-red-600 dark:text-red-400'}`} />
|
||||
) : (
|
||||
<Plug className={`w-4 h-4 ${isReachable ? (isOn ? 'text-bambu-green' : 'text-bambu-gray') : 'text-red-400'}`} />
|
||||
<Plug className={`w-4 h-4 ${isReachable ? (isOn ? 'text-bambu-green' : 'text-bambu-gray') : 'text-red-600 dark:text-red-400'}`} />
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
@@ -67,8 +67,8 @@ function SwitchItem({ plug }: { plug: SmartPlug }) {
|
||||
/* MQTT plugs show power and monitor-only indicator */
|
||||
isReachable ? (
|
||||
<>
|
||||
<Zap className="w-3 h-3 text-teal-400" />
|
||||
<span className="text-teal-400">{Math.round(status?.energy?.power ?? 0)}W</span>
|
||||
<Zap className="w-3 h-3 text-teal-600 dark:text-teal-400" />
|
||||
<span className="text-teal-700 dark:text-teal-400">{Math.round(status?.energy?.power ?? 0)}W</span>
|
||||
<span className="text-bambu-gray mx-1">|</span>
|
||||
<Eye className="w-3 h-3 text-bambu-gray" />
|
||||
<span className="text-bambu-gray">Monitor</span>
|
||||
@@ -86,8 +86,8 @@ function SwitchItem({ plug }: { plug: SmartPlug }) {
|
||||
{status?.energy?.power !== null && status?.energy?.power !== undefined && (
|
||||
<>
|
||||
<span className="text-bambu-gray mx-1">|</span>
|
||||
<Zap className="w-3 h-3 text-yellow-400" />
|
||||
<span className="text-yellow-400">{Math.round(status.energy.power)}W</span>
|
||||
<Zap className="w-3 h-3 text-yellow-600 dark:text-yellow-400" />
|
||||
<span className="text-yellow-700 dark:text-yellow-400">{Math.round(status.energy.power)}W</span>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -7,8 +7,8 @@ const WIKI_TROUBLESHOOTING = 'https://wiki.bambuddy.cool/reference/troubleshooti
|
||||
|
||||
const CATEGORY_META: Record<LogFindingCategory, { icon: ElementType; badgeClass: string }> = {
|
||||
layer8: { icon: Wrench, badgeClass: 'bg-bambu-green/15 text-bambu-green border-bambu-green/30' },
|
||||
environment: { icon: ServerCog, badgeClass: 'bg-amber-500/15 text-amber-300 border-amber-500/30' },
|
||||
bug: { icon: Bug, badgeClass: 'bg-red-500/15 text-red-300 border-red-500/30' },
|
||||
environment: { icon: ServerCog, badgeClass: 'bg-amber-100 dark:bg-amber-500/15 text-amber-700 dark:text-amber-300 border-amber-300 dark:border-amber-500/30' },
|
||||
bug: { icon: Bug, badgeClass: 'bg-red-100 dark:bg-red-500/15 text-red-700 dark:text-red-300 border-red-300 dark:border-red-500/30' },
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -25,7 +25,7 @@ function FindingCard({ finding }: { finding: LogFinding }) {
|
||||
const meta = CATEGORY_META[finding.category] ?? CATEGORY_META.bug;
|
||||
const CategoryIcon = meta.icon;
|
||||
const SeverityIcon = finding.severity === 'error' ? XCircle : AlertTriangle;
|
||||
const severityColor = finding.severity === 'error' ? 'text-red-400' : 'text-amber-400';
|
||||
const severityColor = finding.severity === 'error' ? 'text-red-600 dark:text-red-400' : 'text-amber-600 dark:text-amber-400';
|
||||
|
||||
return (
|
||||
<div className="bg-bambu-dark rounded-lg border border-bambu-dark-tertiary p-4 space-y-2">
|
||||
@@ -82,7 +82,7 @@ export function SystemHealthPanel({ result }: { result: SystemHealthResult }) {
|
||||
|
||||
if (!result.log_available) {
|
||||
return (
|
||||
<div className="rounded-lg bg-amber-500/10 border border-amber-500/30 px-4 py-3 text-sm text-amber-300">
|
||||
<div className="rounded-lg bg-amber-50 dark:bg-amber-500/10 border border-amber-300 dark:border-amber-500/30 px-4 py-3 text-sm text-amber-700 dark:text-amber-300">
|
||||
{t('systemHealth.logUnavailable')}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -223,7 +223,7 @@ export function TagManagementModal({ onClose }: TagManagementModalProps) {
|
||||
) : deleteConfirm === tag.name ? (
|
||||
// Delete confirmation
|
||||
<div className="flex-1 flex items-center gap-2">
|
||||
<AlertTriangle className="w-4 h-4 text-yellow-400 flex-shrink-0" />
|
||||
<AlertTriangle className="w-4 h-4 text-yellow-600 dark:text-yellow-400 flex-shrink-0" />
|
||||
<span className="text-sm text-bambu-gray-light flex-1">
|
||||
Delete "{tag.name}" from {tag.count} archive{tag.count !== 1 ? 's' : ''}?
|
||||
</span>
|
||||
@@ -267,7 +267,7 @@ export function TagManagementModal({ onClose }: TagManagementModalProps) {
|
||||
</button>
|
||||
<button
|
||||
onClick={() => confirmDelete(tag.name)}
|
||||
className="p-1.5 rounded hover:bg-bambu-dark text-bambu-gray hover:text-red-400 transition-colors"
|
||||
className="p-1.5 rounded hover:bg-bambu-dark text-bambu-gray hover:text-red-700 dark:hover:text-red-400 transition-colors"
|
||||
title="Delete tag"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
|
||||
@@ -491,9 +491,9 @@ export function TimelapseEditorModal({
|
||||
|
||||
<button
|
||||
onClick={removeAudio}
|
||||
className="p-2 hover:bg-red-500/20 rounded transition-colors"
|
||||
className="p-2 hover:bg-red-100 dark:hover:bg-red-500/20 rounded transition-colors"
|
||||
>
|
||||
<Trash2 className="w-4 h-4 text-red-400" />
|
||||
<Trash2 className="w-4 h-4 text-red-600 dark:text-red-400" />
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
|
||||
@@ -46,8 +46,8 @@ function BackupCodesDisplay({ codes, onDone }: { codes: string[]; onDone: () =>
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="bg-amber-500/10 border border-amber-500/30 rounded-lg p-4">
|
||||
<p className="text-amber-400 text-sm font-medium">{t('settings.twoFa.backupCodesWarning')}</p>
|
||||
<div className="bg-amber-50 dark:bg-amber-500/10 border border-amber-300 dark:border-amber-500/30 rounded-lg p-4">
|
||||
<p className="text-amber-700 dark:text-amber-400 text-sm font-medium">{t('settings.twoFa.backupCodesWarning')}</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{codes.map((code, index) => (
|
||||
@@ -289,8 +289,8 @@ export function TwoFactorSettings() {
|
||||
<Card id="card-2fa-totp">
|
||||
<CardHeader>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={`w-10 h-10 rounded-full flex items-center justify-center ${status?.totp_enabled ? 'bg-green-500/20' : 'bg-gray-500/20'}`}>
|
||||
<Smartphone className={`w-5 h-5 ${status?.totp_enabled ? 'text-green-400' : 'text-gray-400'}`} />
|
||||
<div className={`w-10 h-10 rounded-full flex items-center justify-center ${status?.totp_enabled ? 'bg-green-100 dark:bg-green-500/20' : 'bg-gray-500/20'}`}>
|
||||
<Smartphone className={`w-5 h-5 ${status?.totp_enabled ? 'text-green-600 dark:text-green-400' : 'text-gray-400'}`} />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-white font-semibold">{t('settings.twoFa.totpTitle')}</h3>
|
||||
@@ -298,7 +298,7 @@ export function TwoFactorSettings() {
|
||||
</div>
|
||||
<div className="ml-auto">
|
||||
{status?.totp_enabled ? (
|
||||
<span className="flex items-center gap-1 text-green-400 text-sm font-medium">
|
||||
<span className="flex items-center gap-1 text-green-700 dark:text-green-400 text-sm font-medium">
|
||||
<ShieldCheck className="w-4 h-4" /> {t('common.enabled')}
|
||||
</span>
|
||||
) : (
|
||||
@@ -394,8 +394,8 @@ export function TwoFactorSettings() {
|
||||
<Card id="card-2fa-emailotp">
|
||||
<CardHeader>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={`w-10 h-10 rounded-full flex items-center justify-center ${status?.email_otp_enabled ? 'bg-green-500/20' : 'bg-gray-500/20'}`}>
|
||||
<Mail className={`w-5 h-5 ${status?.email_otp_enabled ? 'text-green-400' : 'text-gray-400'}`} />
|
||||
<div className={`w-10 h-10 rounded-full flex items-center justify-center ${status?.email_otp_enabled ? 'bg-green-100 dark:bg-green-500/20' : 'bg-gray-500/20'}`}>
|
||||
<Mail className={`w-5 h-5 ${status?.email_otp_enabled ? 'text-green-600 dark:text-green-400' : 'text-gray-400'}`} />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h3 className="text-white font-semibold">{t('settings.twoFa.emailOtpTitle')}</h3>
|
||||
@@ -408,7 +408,7 @@ export function TwoFactorSettings() {
|
||||
{/* Show status badge; enable/disable handled in CardContent */}
|
||||
<div className="ml-auto">
|
||||
{status?.email_otp_enabled ? (
|
||||
<span className="flex items-center gap-1 text-green-400 text-sm font-medium">
|
||||
<span className="flex items-center gap-1 text-green-700 dark:text-green-400 text-sm font-medium">
|
||||
<ShieldCheck className="w-4 h-4" /> {t('common.enabled')}
|
||||
</span>
|
||||
) : (
|
||||
@@ -421,7 +421,7 @@ export function TwoFactorSettings() {
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{!hasEmail ? (
|
||||
<p className="text-amber-400 text-sm">{t('settings.twoFa.addEmailFirst')}</p>
|
||||
<p className="text-amber-700 dark:text-amber-400 text-sm">{t('settings.twoFa.addEmailFirst')}</p>
|
||||
) : emailSetupToken ? (
|
||||
/* Step 2: enter the code that was sent to the email */
|
||||
<div className="space-y-4">
|
||||
|
||||
@@ -222,7 +222,7 @@ export function UploadModal({ onClose, initialFiles }: UploadModalProps) {
|
||||
{f.status === 'pending' && (
|
||||
<button
|
||||
onClick={() => removeFile(index)}
|
||||
className="text-bambu-gray hover:text-red-400 transition-colors"
|
||||
className="text-bambu-gray hover:text-red-600 dark:hover:text-red-400 transition-colors"
|
||||
disabled={isUploading}
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
@@ -236,8 +236,8 @@ export function UploadModal({ onClose, initialFiles }: UploadModalProps) {
|
||||
)}
|
||||
{f.status === 'error' && (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-red-400">{f.error}</span>
|
||||
<AlertCircle className="w-4 h-4 text-red-400" />
|
||||
<span className="text-xs text-red-700 dark:text-red-400">{f.error}</span>
|
||||
<AlertCircle className="w-4 h-4 text-red-600 dark:text-red-400" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -253,7 +253,7 @@ export function UploadModal({ onClose, initialFiles }: UploadModalProps) {
|
||||
<p className="text-sm text-white">
|
||||
<span className="text-bambu-green">{uploadResult.uploaded}</span> {t('uploadModal.uploaded')}
|
||||
{uploadResult.failed > 0 && (
|
||||
<>, <span className="text-red-400">{uploadResult.failed}</span> {t('uploadModal.failed')}</>
|
||||
<>, <span className="text-red-700 dark:text-red-400">{uploadResult.failed}</span> {t('uploadModal.failed')}</>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -318,7 +318,7 @@ export function VirtualPrinterCard({ printer, models }: VirtualPrinterCardProps)
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setShowDeleteConfirm(true)}
|
||||
className="p-1.5 text-bambu-gray hover:text-red-400 transition-colors flex-shrink-0"
|
||||
className="p-1.5 text-bambu-gray hover:text-red-600 dark:hover:text-red-400 transition-colors flex-shrink-0"
|
||||
title={t('common.delete')}
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
@@ -329,7 +329,7 @@ export function VirtualPrinterCard({ printer, models }: VirtualPrinterCardProps)
|
||||
Shown only when this VP is marked Tailscale-exposed AND the daemon is up. */}
|
||||
<div className="flex items-center gap-2 -mt-2">
|
||||
{tailscaleFqdn && (
|
||||
<span className="flex items-center gap-1 text-green-400/70 min-w-0">
|
||||
<span className="flex items-center gap-1 text-green-700/80 dark:text-green-400/70 min-w-0">
|
||||
<ShieldCheck className="w-3.5 h-3.5 flex-shrink-0" />
|
||||
<span className="font-mono text-xs truncate">
|
||||
{tailscaleIp ? `${tailscaleIp} (${tailscaleFqdn})` : tailscaleFqdn}
|
||||
@@ -521,8 +521,8 @@ export function VirtualPrinterCard({ printer, models }: VirtualPrinterCardProps)
|
||||
{/* Proxy mode: hint about using target printer's access code */}
|
||||
{localMode === 'proxy' && (
|
||||
<div className="pt-2 border-t border-bambu-dark-tertiary">
|
||||
<div className="flex items-start gap-2 p-2 rounded bg-blue-500/10 border border-blue-500/30">
|
||||
<Info className="w-4 h-4 text-blue-400 flex-shrink-0 mt-0.5" />
|
||||
<div className="flex items-start gap-2 p-2 rounded bg-blue-50 border border-blue-300 dark:bg-blue-500/10 dark:border-blue-500/30">
|
||||
<Info className="w-4 h-4 text-blue-600 dark:text-blue-400 flex-shrink-0 mt-0.5" />
|
||||
<p className="text-xs text-bambu-gray">
|
||||
{t('virtualPrinter.proxy.accessCodeHint')}
|
||||
</p>
|
||||
@@ -536,17 +536,17 @@ export function VirtualPrinterCard({ printer, models }: VirtualPrinterCardProps)
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<div className="text-white text-sm font-medium">{t('virtualPrinter.accessCode.title')}</div>
|
||||
{inheritsAccessCodeFromTarget ? (
|
||||
<span className="flex items-center gap-1 text-xs text-blue-400">
|
||||
<span className="flex items-center gap-1 text-xs text-blue-700 dark:text-blue-400">
|
||||
<Info className="w-3 h-3" />
|
||||
{t('virtualPrinter.accessCode.inheritedFromTarget')}
|
||||
</span>
|
||||
) : printer.access_code_set ? (
|
||||
<span className="flex items-center gap-1 text-xs text-green-400">
|
||||
<span className="flex items-center gap-1 text-xs text-green-700 dark:text-green-400">
|
||||
<Check className="w-3 h-3" />
|
||||
{t('virtualPrinter.accessCode.isSet')}
|
||||
</span>
|
||||
) : (
|
||||
<span className="flex items-center gap-1 text-xs text-yellow-400">
|
||||
<span className="flex items-center gap-1 text-xs text-yellow-700 dark:text-yellow-400">
|
||||
<AlertTriangle className="w-3 h-3" />
|
||||
{t('virtualPrinter.accessCode.notSet')}
|
||||
</span>
|
||||
@@ -603,7 +603,7 @@ export function VirtualPrinterCard({ printer, models }: VirtualPrinterCardProps)
|
||||
</div>
|
||||
{localAccessCode && (
|
||||
<p className="text-xs text-bambu-gray mt-1">
|
||||
<span className={localAccessCode.length === 8 ? 'text-green-400' : 'text-yellow-400'}>
|
||||
<span className={localAccessCode.length === 8 ? 'text-green-700 dark:text-green-400' : 'text-yellow-700 dark:text-yellow-400'}>
|
||||
{t('virtualPrinter.accessCode.charCount', { count: localAccessCode.length })}
|
||||
</span>
|
||||
</p>
|
||||
@@ -666,7 +666,7 @@ export function VirtualPrinterCard({ printer, models }: VirtualPrinterCardProps)
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<div className="text-white text-sm font-medium">{t('virtualPrinter.remoteInterface.title')}</div>
|
||||
{localRemoteInterfaceIp ? (
|
||||
<span className="flex items-center gap-1 text-xs text-green-400"><Check className="w-3 h-3" /></span>
|
||||
<span className="flex items-center gap-1 text-xs text-green-700 dark:text-green-400"><Check className="w-3 h-3" /></span>
|
||||
) : (
|
||||
<span className="flex items-center gap-1 text-xs text-bambu-gray" title={t('virtualPrinter.remoteInterface.optional')}><Info className="w-3 h-3" /></span>
|
||||
)}
|
||||
|
||||
@@ -19,8 +19,8 @@ import {
|
||||
|
||||
function StatusIcon({ status }: { status: VPDiagnosticStatus }) {
|
||||
if (status === 'pass') return <CheckCircle2 className="w-5 h-5 text-bambu-green flex-shrink-0" />;
|
||||
if (status === 'fail') return <XCircle className="w-5 h-5 text-red-400 flex-shrink-0" />;
|
||||
if (status === 'warn') return <AlertTriangle className="w-5 h-5 text-amber-400 flex-shrink-0" />;
|
||||
if (status === 'fail') return <XCircle className="w-5 h-5 text-red-600 dark:text-red-400 flex-shrink-0" />;
|
||||
if (status === 'warn') return <AlertTriangle className="w-5 h-5 text-amber-600 dark:text-amber-400 flex-shrink-0" />;
|
||||
return <MinusCircle className="w-5 h-5 text-bambu-gray flex-shrink-0" />;
|
||||
}
|
||||
|
||||
@@ -64,8 +64,8 @@ export function VirtualPrinterDiagnosticModal({
|
||||
result?.overall === 'ok'
|
||||
? 'bg-bambu-green/10 border-bambu-green/30 text-bambu-green'
|
||||
: result?.overall === 'warnings'
|
||||
? 'bg-amber-500/10 border-amber-500/30 text-amber-300'
|
||||
: 'bg-red-500/10 border-red-500/30 text-red-300';
|
||||
? 'bg-amber-50 dark:bg-amber-500/10 border-amber-300 dark:border-amber-500/30 text-amber-700 dark:text-amber-300'
|
||||
: 'bg-red-50 dark:bg-red-500/10 border-red-300 dark:border-red-500/30 text-red-700 dark:text-red-300';
|
||||
|
||||
const renderCheck = (check: VPDiagnosticCheck) => {
|
||||
const detail = t(`vpDiagnostic.check.${check.id}.${check.status}`, {
|
||||
@@ -123,7 +123,7 @@ export function VirtualPrinterDiagnosticModal({
|
||||
)}
|
||||
|
||||
{diagnose.isError && (
|
||||
<div className="rounded-lg bg-red-500/10 border border-red-500/30 px-4 py-3 text-sm text-red-300">
|
||||
<div className="rounded-lg bg-red-50 dark:bg-red-500/10 border border-red-300 dark:border-red-500/30 px-4 py-3 text-sm text-red-700 dark:text-red-300">
|
||||
{t('vpDiagnostic.runFailed', { error: (diagnose.error as Error).message })}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -96,7 +96,7 @@ export function VirtualPrinterList() {
|
||||
href="https://wiki.bambuddy.cool/features/virtual-printer/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-1.5 mt-2 px-3 py-1.5 bg-yellow-500/20 border border-yellow-500/50 rounded text-yellow-400 hover:bg-yellow-500/30 transition-colors text-xs"
|
||||
className="inline-flex items-center gap-1.5 mt-2 px-3 py-1.5 bg-yellow-100 dark:bg-yellow-500/20 border border-yellow-500/50 rounded text-yellow-700 dark:text-yellow-400 hover:bg-yellow-500/30 transition-colors text-xs"
|
||||
>
|
||||
<ExternalLink className="w-3 h-3" />
|
||||
{t('virtualPrinter.setupRequired.readGuide')}
|
||||
@@ -109,7 +109,7 @@ export function VirtualPrinterList() {
|
||||
<Card className="lg:col-span-2">
|
||||
<CardContent className="py-3 px-4">
|
||||
<div className="flex items-start gap-2">
|
||||
<Info className="w-4 h-4 text-blue-400 flex-shrink-0 mt-0.5" />
|
||||
<Info className="w-4 h-4 text-blue-600 dark:text-blue-400 flex-shrink-0 mt-0.5" />
|
||||
<div className="text-xs text-bambu-gray">
|
||||
<p className="text-white font-medium mb-1">{t('virtualPrinter.howItWorks.title')}</p>
|
||||
<ul className="space-y-1 list-disc list-inside">
|
||||
|
||||
@@ -192,7 +192,7 @@ export function VirtualPrinterSettings() {
|
||||
<h2 className="text-lg font-semibold text-white">{t('virtualPrinter.title')}</h2>
|
||||
</div>
|
||||
{status && (
|
||||
<div className={`flex items-center gap-2 text-sm ${isRunning ? 'text-green-400' : 'text-bambu-gray'}`}>
|
||||
<div className={`flex items-center gap-2 text-sm ${isRunning ? 'text-green-700 dark:text-green-400' : 'text-bambu-gray'}`}>
|
||||
<span className={`w-2 h-2 rounded-full ${isRunning ? 'bg-green-400 animate-pulse' : 'bg-gray-500'}`} />
|
||||
{isRunning ? t('virtualPrinter.running') : t('virtualPrinter.stopped')}
|
||||
</div>
|
||||
@@ -272,12 +272,12 @@ export function VirtualPrinterSettings() {
|
||||
<div className="text-white font-medium mb-2">{t('virtualPrinter.accessCode.title')}</div>
|
||||
<div className="text-sm text-bambu-gray mb-3">
|
||||
{settings?.access_code_set ? (
|
||||
<span className="flex items-center gap-1 text-green-400">
|
||||
<span className="flex items-center gap-1 text-green-700 dark:text-green-400">
|
||||
<Check className="w-4 h-4" />
|
||||
{t('virtualPrinter.accessCode.isSet')}
|
||||
</span>
|
||||
) : (
|
||||
<span className="flex items-center gap-1 text-yellow-400">
|
||||
<span className="flex items-center gap-1 text-yellow-700 dark:text-yellow-400">
|
||||
<AlertTriangle className="w-4 h-4" />
|
||||
{t('virtualPrinter.accessCode.notSet')}
|
||||
</span>
|
||||
@@ -311,7 +311,7 @@ export function VirtualPrinterSettings() {
|
||||
<p className="text-xs text-bambu-gray mt-2">
|
||||
{t('virtualPrinter.accessCode.hint')}
|
||||
{localAccessCode && (
|
||||
<span className={localAccessCode.length === 8 ? 'text-green-400' : 'text-yellow-400'}>
|
||||
<span className={localAccessCode.length === 8 ? 'text-green-700 dark:text-green-400' : 'text-yellow-700 dark:text-yellow-400'}>
|
||||
{' '}{t('virtualPrinter.accessCode.charCount', { count: localAccessCode.length })}
|
||||
</span>
|
||||
)}
|
||||
@@ -325,12 +325,12 @@ export function VirtualPrinterSettings() {
|
||||
<div className="text-white font-medium mb-2">{t('virtualPrinter.targetPrinter.title')}</div>
|
||||
<div className="text-sm text-bambu-gray mb-3">
|
||||
{localTargetPrinterId ? (
|
||||
<span className="flex items-center gap-1 text-green-400">
|
||||
<span className="flex items-center gap-1 text-green-700 dark:text-green-400">
|
||||
<Check className="w-4 h-4" />
|
||||
{t('virtualPrinter.targetPrinter.configured')}
|
||||
</span>
|
||||
) : (
|
||||
<span className="flex items-center gap-1 text-yellow-400">
|
||||
<span className="flex items-center gap-1 text-yellow-700 dark:text-yellow-400">
|
||||
<AlertTriangle className="w-4 h-4" />
|
||||
{t('virtualPrinter.targetPrinter.notConfigured')}
|
||||
</span>
|
||||
@@ -361,7 +361,7 @@ export function VirtualPrinterSettings() {
|
||||
{t('virtualPrinter.targetPrinter.hint')}
|
||||
</p>
|
||||
{!printers?.length && (
|
||||
<p className="text-xs text-yellow-400 mt-2">
|
||||
<p className="text-xs text-yellow-700 dark:text-yellow-400 mt-2">
|
||||
<AlertTriangle className="w-3 h-3 inline mr-1" />
|
||||
{t('virtualPrinter.targetPrinter.noPrinters')}
|
||||
</p>
|
||||
@@ -375,7 +375,7 @@ export function VirtualPrinterSettings() {
|
||||
<div className="text-white font-medium mb-2">{t('virtualPrinter.remoteInterface.title')}</div>
|
||||
<div className="text-sm text-bambu-gray mb-3">
|
||||
{localRemoteInterfaceIp ? (
|
||||
<span className="flex items-center gap-1 text-green-400">
|
||||
<span className="flex items-center gap-1 text-green-700 dark:text-green-400">
|
||||
<Check className="w-4 h-4" />
|
||||
{t('virtualPrinter.remoteInterface.configured')}
|
||||
</span>
|
||||
@@ -487,7 +487,7 @@ export function VirtualPrinterSettings() {
|
||||
href="https://wiki.bambuddy.cool/features/virtual-printer/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-2 px-4 py-2 bg-yellow-500/20 border border-yellow-500/50 rounded-md text-yellow-400 hover:bg-yellow-500/30 transition-colors"
|
||||
className="inline-flex items-center gap-2 px-4 py-2 bg-yellow-100 dark:bg-yellow-500/20 border border-yellow-500/50 rounded-md text-yellow-700 dark:text-yellow-400 hover:bg-yellow-500/30 transition-colors"
|
||||
>
|
||||
<ExternalLink className="w-4 h-4" />
|
||||
{t('virtualPrinter.setupRequired.readGuide')}
|
||||
@@ -501,7 +501,7 @@ export function VirtualPrinterSettings() {
|
||||
<Card>
|
||||
<CardContent className="py-4">
|
||||
<div className="flex items-start gap-3">
|
||||
<Info className="w-5 h-5 text-blue-400 flex-shrink-0 mt-0.5" />
|
||||
<Info className="w-5 h-5 text-blue-600 dark:text-blue-400 flex-shrink-0 mt-0.5" />
|
||||
<div className="text-sm text-bambu-gray">
|
||||
<p className="mb-2">
|
||||
<strong className="text-white">{localMode === 'proxy' ? t('virtualPrinter.howItWorks.titleProxy') : t('virtualPrinter.howItWorks.title')}:</strong>
|
||||
|
||||
@@ -495,7 +495,7 @@ export function ColorSection({
|
||||
data-testid="extra-colors-input"
|
||||
/>
|
||||
{extraColorsErrors.length > 0 && (
|
||||
<p className="text-xs text-red-400 mt-1">
|
||||
<p className="text-xs text-red-700 dark:text-red-400 mt-1">
|
||||
{t('inventory.extraColorsInvalid', { tokens: extraColorsErrors.join(', ') })}
|
||||
</p>
|
||||
)}
|
||||
|
||||
@@ -186,7 +186,7 @@ export function FilamentSection({
|
||||
</div>
|
||||
)}
|
||||
{errors?.slicer_filament && (
|
||||
<p className="mt-1 text-xs text-red-400">{errors.slicer_filament}</p>
|
||||
<p className="mt-1 text-xs text-red-700 dark:text-red-400">{errors.slicer_filament}</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
@@ -250,7 +250,7 @@ export function FilamentSection({
|
||||
)}
|
||||
</div>
|
||||
{errors?.material && (
|
||||
<p className="mt-1 text-xs text-red-400">{errors.material}</p>
|
||||
<p className="mt-1 text-xs text-red-700 dark:text-red-400">{errors.material}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -315,7 +315,7 @@ export function FilamentSection({
|
||||
)}
|
||||
</div>
|
||||
{errors?.brand && (
|
||||
<p className="mt-1 text-xs text-red-400">{errors.brand}</p>
|
||||
<p className="mt-1 text-xs text-red-700 dark:text-red-400">{errors.brand}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -379,7 +379,7 @@ export function FilamentSection({
|
||||
)}
|
||||
</div>
|
||||
{errors?.subtype && (
|
||||
<p className="mt-1 text-xs text-red-400">{errors.subtype}</p>
|
||||
<p className="mt-1 text-xs text-red-700 dark:text-red-400">{errors.subtype}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -973,7 +973,7 @@ function ArchiveCard({
|
||||
className={`text-[10px] px-1.5 py-0.5 rounded font-medium ${
|
||||
isSlicedFile(archive)
|
||||
? 'bg-bambu-green/20 text-bambu-green'
|
||||
: 'bg-orange-500/20 text-orange-400'
|
||||
: 'bg-orange-100 dark:bg-orange-500/20 text-orange-700 dark:text-orange-400'
|
||||
}`}
|
||||
title={
|
||||
isSlicedFile(archive)
|
||||
@@ -1039,8 +1039,8 @@ function ArchiveCard({
|
||||
archive.time_accuracy >= 95 && archive.time_accuracy <= 105
|
||||
? 'bg-bambu-green/20 text-bambu-green'
|
||||
: archive.time_accuracy > 105
|
||||
? 'bg-blue-500/20 text-blue-400'
|
||||
: 'bg-orange-500/20 text-orange-400'
|
||||
? 'bg-blue-100 dark:bg-blue-500/20 text-blue-700 dark:text-blue-400'
|
||||
: 'bg-orange-100 dark:bg-orange-500/20 text-orange-700 dark:text-orange-400'
|
||||
}`}>
|
||||
{archive.time_accuracy > 100 ? '+' : ''}{(archive.time_accuracy - 100).toFixed(0)}%
|
||||
</span>
|
||||
@@ -1113,7 +1113,7 @@ function ArchiveCard({
|
||||
<div className="flex flex-wrap items-center gap-1.5 mb-3">
|
||||
{archive.notes && (
|
||||
<div
|
||||
className="flex items-center gap-1 px-1.5 py-0.5 bg-blue-500/20 text-blue-400 rounded text-xs"
|
||||
className="flex items-center gap-1 px-1.5 py-0.5 bg-blue-100 dark:bg-blue-500/20 text-blue-700 dark:text-blue-400 rounded text-xs"
|
||||
title={archive.notes}
|
||||
>
|
||||
<StickyNote className="w-3 h-3" />
|
||||
@@ -1240,7 +1240,7 @@ function ArchiveCard({
|
||||
disabled={!canModify('archives', 'delete', archive.created_by_id)}
|
||||
title={!canModify('archives', 'delete', archive.created_by_id) ? t('archives.card.noPermissionDelete') : t('archives.card.delete')}
|
||||
>
|
||||
<Trash2 className="w-3 h-3 sm:w-4 sm:h-4 text-red-400" />
|
||||
<Trash2 className="w-3 h-3 sm:w-4 sm:h-4 text-red-600 dark:text-red-400" />
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
@@ -1325,8 +1325,8 @@ function ArchiveCard({
|
||||
<div
|
||||
className={
|
||||
(deleteImpactQuery.data?.currently_printing ?? 0) > 0
|
||||
? 'text-sm text-red-400 mb-2'
|
||||
: 'text-sm text-amber-400 mb-2'
|
||||
? 'text-sm text-red-600 dark:text-red-400 mb-2'
|
||||
: 'text-sm text-amber-600 dark:text-amber-400 mb-2'
|
||||
}
|
||||
>
|
||||
{(deleteImpactQuery.data?.currently_printing ?? 0) > 0
|
||||
@@ -2139,7 +2139,7 @@ function ArchiveListRow({
|
||||
title={t('archives.card.openFolder', { name: linkedFolders[0].name })}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<FolderOpen className="w-3.5 h-3.5 text-yellow-400" />
|
||||
<FolderOpen className="w-3.5 h-3.5 text-yellow-600 dark:text-yellow-400" />
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
@@ -2255,7 +2255,7 @@ function ArchiveListRow({
|
||||
disabled={!canModify('archives', 'delete', archive.created_by_id)}
|
||||
title={!canModify('archives', 'delete', archive.created_by_id) ? t('archives.card.noPermissionDelete') : t('archives.card.delete')}
|
||||
>
|
||||
<Trash2 className="w-4 h-4 text-red-400" />
|
||||
<Trash2 className="w-4 h-4 text-red-600 dark:text-red-400" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
@@ -2351,8 +2351,8 @@ function ArchiveListRow({
|
||||
<div
|
||||
className={
|
||||
(deleteImpactQuery.data?.currently_printing ?? 0) > 0
|
||||
? 'text-sm text-red-400 mb-2'
|
||||
: 'text-sm text-amber-400 mb-2'
|
||||
? 'text-sm text-red-600 dark:text-red-400 mb-2'
|
||||
: 'text-sm text-amber-600 dark:text-amber-400 mb-2'
|
||||
}
|
||||
>
|
||||
{(deleteImpactQuery.data?.currently_printing ?? 0) > 0
|
||||
@@ -3272,19 +3272,19 @@ export function ArchivesPage() {
|
||||
)}
|
||||
|
||||
{no3MFWarning?.has_fallback && !no3MFWarningDismissed && (
|
||||
<div className="mb-6 rounded-lg border border-amber-500/30 bg-amber-500/10 px-4 py-3 flex items-start gap-3">
|
||||
<AlertCircle className="w-5 h-5 text-amber-400 flex-shrink-0 mt-0.5" />
|
||||
<div className="mb-6 rounded-lg border border-amber-300 dark:border-amber-500/30 bg-amber-50 dark:bg-amber-500/10 px-4 py-3 flex items-start gap-3">
|
||||
<AlertCircle className="w-5 h-5 text-amber-600 dark:text-amber-400 flex-shrink-0 mt-0.5" />
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="text-sm font-medium text-amber-200">
|
||||
<div className="text-sm font-medium text-amber-900 dark:text-amber-200">
|
||||
{t('archives.no3mfBanner.title')}
|
||||
</div>
|
||||
<div className="text-xs text-amber-200/80 mt-1">
|
||||
<div className="text-xs text-amber-800/90 dark:text-amber-200/80 mt-1">
|
||||
{t('archives.no3mfBanner.body')}{' '}
|
||||
<a
|
||||
href="https://wiki.bambuddy.cool/getting-started/#step-4-enable-store-sent-files-on-external-storage"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="underline hover:text-amber-100 inline-flex items-center gap-1"
|
||||
className="underline hover:text-amber-900 dark:hover:text-amber-100 inline-flex items-center gap-1"
|
||||
>
|
||||
{t('archives.no3mfBanner.docsLink')}
|
||||
<ExternalLink className="w-3 h-3" />
|
||||
@@ -3293,7 +3293,7 @@ export function ArchivesPage() {
|
||||
</div>
|
||||
<button
|
||||
onClick={dismissNo3MFWarning}
|
||||
className="text-amber-200/60 hover:text-amber-200 flex-shrink-0 p-1 -m-1"
|
||||
className="text-amber-800/70 dark:text-amber-200/60 hover:text-amber-900 dark:hover:text-amber-200 flex-shrink-0 p-1 -m-1"
|
||||
title={t('archives.no3mfBanner.dismissLabel')}
|
||||
aria-label={t('archives.no3mfBanner.dismissLabel')}
|
||||
>
|
||||
@@ -3542,7 +3542,7 @@ export function ArchivesPage() {
|
||||
onClick={() => setFilterFavorites(!filterFavorites)}
|
||||
className={`flex items-center gap-2 px-3 py-2 rounded-lg border transition-colors flex-shrink-0 ${
|
||||
filterFavorites
|
||||
? 'bg-yellow-500/20 border-yellow-500 text-yellow-400'
|
||||
? 'bg-yellow-100 dark:bg-yellow-500/20 border-yellow-500 text-yellow-700 dark:text-yellow-400'
|
||||
: 'bg-bambu-dark border-bambu-dark-tertiary text-bambu-gray hover:text-white'
|
||||
}`}
|
||||
title={filterFavorites ? t('archives.showAll') : t('archives.showFavoritesOnly')}
|
||||
@@ -3554,7 +3554,7 @@ export function ArchivesPage() {
|
||||
onClick={() => setHideFailed(!hideFailed)}
|
||||
className={`flex items-center gap-2 px-3 py-2 rounded-lg border transition-colors flex-shrink-0 ${
|
||||
hideFailed
|
||||
? 'bg-red-500/20 border-red-500 text-red-400'
|
||||
? 'bg-red-100 dark:bg-red-500/20 border-red-500 text-red-700 dark:text-red-400'
|
||||
: 'bg-bambu-dark border-bambu-dark-tertiary text-bambu-gray hover:text-white'
|
||||
}`}
|
||||
title={hideFailed ? t('archives.showFailedPrints') : t('archives.hideFailedPrints')}
|
||||
@@ -3566,7 +3566,7 @@ export function ArchivesPage() {
|
||||
onClick={() => setHideDuplicates(!hideDuplicates)}
|
||||
className={`flex items-center gap-2 px-3 py-2 rounded-lg border transition-colors flex-shrink-0 ${
|
||||
hideDuplicates
|
||||
? 'bg-purple-500/20 border-purple-500 text-purple-400'
|
||||
? 'bg-purple-100 dark:bg-purple-500/20 border-purple-500 text-purple-700 dark:text-purple-400'
|
||||
: 'bg-bambu-dark border-bambu-dark-tertiary text-bambu-gray hover:text-white'
|
||||
}`}
|
||||
title={t('archives.hideDuplicates')}
|
||||
@@ -3914,11 +3914,11 @@ export function ArchivesPage() {
|
||||
<td className="px-4 py-3 text-bambu-gray-light">{entry.created_by_username || '—'}</td>
|
||||
<td className="px-4 py-3">
|
||||
<span className={`inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium ${
|
||||
entry.status === 'completed' ? 'bg-green-500/20 text-green-400' :
|
||||
entry.status === 'failed' ? 'bg-red-500/20 text-red-400' :
|
||||
entry.status === 'stopped' ? 'bg-yellow-500/20 text-yellow-400' :
|
||||
entry.status === 'cancelled' ? 'bg-orange-500/20 text-orange-400' :
|
||||
entry.status === 'skipped' ? 'bg-blue-500/20 text-blue-400' :
|
||||
entry.status === 'completed' ? 'bg-green-100 dark:bg-green-500/20 text-green-700 dark:text-green-400' :
|
||||
entry.status === 'failed' ? 'bg-red-100 dark:bg-red-500/20 text-red-700 dark:text-red-400' :
|
||||
entry.status === 'stopped' ? 'bg-yellow-100 dark:bg-yellow-500/20 text-yellow-700 dark:text-yellow-400' :
|
||||
entry.status === 'cancelled' ? 'bg-orange-100 dark:bg-orange-500/20 text-orange-700 dark:text-orange-400' :
|
||||
entry.status === 'skipped' ? 'bg-blue-100 dark:bg-blue-500/20 text-blue-700 dark:text-blue-400' :
|
||||
'bg-gray-500/20 text-gray-400'
|
||||
}`}>
|
||||
{entry.status}
|
||||
@@ -3988,7 +3988,7 @@ export function ArchivesPage() {
|
||||
? t('archives.log.deleteEntryTitle')
|
||||
: t('archives.permission.noDelete')
|
||||
}
|
||||
className="text-bambu-gray hover:text-red-400 disabled:opacity-40 disabled:hover:text-bambu-gray transition-colors"
|
||||
className="text-bambu-gray hover:text-red-600 dark:hover:text-red-400 disabled:opacity-40 disabled:hover:text-bambu-gray transition-colors"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</button>
|
||||
|
||||
@@ -141,7 +141,7 @@ function ConfirmRevokeModal({ token, onConfirm, onCancel }: ConfirmRevokeModalPr
|
||||
>
|
||||
<div className="bg-bambu-dark-secondary rounded-lg p-6 max-w-md w-full border border-red-500/40">
|
||||
<div className="flex items-start gap-3 mb-4">
|
||||
<AlertTriangle className="w-6 h-6 text-red-400 flex-shrink-0 mt-0.5" />
|
||||
<AlertTriangle className="w-6 h-6 text-red-600 dark:text-red-400 flex-shrink-0 mt-0.5" />
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-white">
|
||||
{t('cameraTokens.confirmRevoke.title', 'Revoke this token?')}
|
||||
@@ -217,7 +217,7 @@ function JustCreatedModal({ token, onClose }: JustCreatedModalProps) {
|
||||
<div className="fixed inset-0 bg-black/60 flex items-center justify-center z-50 p-4">
|
||||
<div className="bg-bambu-dark-secondary rounded-lg p-6 max-w-2xl w-full border border-bambu-green/40">
|
||||
<div className="flex items-start gap-3 mb-4">
|
||||
<AlertTriangle className="w-6 h-6 text-yellow-400 flex-shrink-0 mt-0.5" />
|
||||
<AlertTriangle className="w-6 h-6 text-yellow-600 dark:text-yellow-400 flex-shrink-0 mt-0.5" />
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-white">
|
||||
{t('cameraTokens.created.title', 'Token created — copy it now')}
|
||||
@@ -273,10 +273,10 @@ function TokenRow({ token, showOwner, ownerLabel, onRevoke }: TokenRowProps) {
|
||||
{showOwner && <td className="py-3 px-3 text-bambu-gray">{ownerLabel}</td>}
|
||||
<td className="py-3 px-3 text-bambu-gray font-mono text-xs">{token.lookup_prefix}…</td>
|
||||
<td className="py-3 px-3 text-bambu-gray">{formatDate(token.created_at)}</td>
|
||||
<td className={`py-3 px-3 ${expired ? 'text-red-400' : 'text-bambu-gray'}`}>
|
||||
<td className={`py-3 px-3 ${expired ? 'text-red-700 dark:text-red-400' : 'text-bambu-gray'}`}>
|
||||
{formatDate(token.expires_at)}
|
||||
{expired && (
|
||||
<span className="ml-2 px-2 py-0.5 text-xs bg-red-500/20 text-red-300 rounded">
|
||||
<span className="ml-2 px-2 py-0.5 text-xs bg-red-100 dark:bg-red-500/20 text-red-700 dark:text-red-300 rounded">
|
||||
{t('cameraTokens.list.expired', 'Expired')}
|
||||
</span>
|
||||
)}
|
||||
@@ -286,7 +286,7 @@ function TokenRow({ token, showOwner, ownerLabel, onRevoke }: TokenRowProps) {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onRevoke(token.id)}
|
||||
className="inline-flex items-center gap-1 px-2 py-1 text-sm text-red-400 hover:text-red-300"
|
||||
className="inline-flex items-center gap-1 px-2 py-1 text-sm text-red-700 dark:text-red-400 hover:text-red-900 dark:hover:text-red-300"
|
||||
title={t('cameraTokens.list.revoke', 'Revoke')}
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
|
||||
@@ -294,7 +294,7 @@ function RenameModal({ type, currentName, onClose, onSave, isLoading, t }: Renam
|
||||
)}
|
||||
</div>
|
||||
{filenameError && (
|
||||
<p className="mt-1 text-xs text-red-400">{filenameError}</p>
|
||||
<p className="mt-1 text-xs text-red-700 dark:text-red-400">{filenameError}</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex justify-end gap-2 pt-2">
|
||||
@@ -593,7 +593,7 @@ function FolderTreeItem({ folder, selectedFolderId, onSelect, onDelete, onLink,
|
||||
<div className="w-4.5" />
|
||||
)}
|
||||
{isExternal ? (
|
||||
<FolderSymlink className="w-4 h-4 text-purple-400 flex-shrink-0" />
|
||||
<FolderSymlink className="w-4 h-4 text-purple-600 dark:text-purple-400 flex-shrink-0" />
|
||||
) : (
|
||||
<FolderOpen className="w-4 h-4 text-bambu-green flex-shrink-0" />
|
||||
)}
|
||||
@@ -602,7 +602,7 @@ function FolderTreeItem({ folder, selectedFolderId, onSelect, onDelete, onLink,
|
||||
{isLinked && (
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); onLink(folder); }}
|
||||
className="flex-shrink-0 flex items-center gap-1 text-xs px-1.5 py-0.5 rounded bg-blue-500/20 text-blue-400 hover:bg-blue-500/30 transition-colors"
|
||||
className="flex-shrink-0 flex items-center gap-1 text-xs px-1.5 py-0.5 rounded bg-blue-100 dark:bg-blue-500/20 text-blue-700 dark:text-blue-400 hover:bg-blue-200 dark:hover:bg-blue-500/30 transition-colors"
|
||||
title={`${folder.project_name ? `Project: ${folder.project_name}` : `Archive: ${folder.archive_name}`} (click to change)`}
|
||||
>
|
||||
<Link2 className="w-3 h-3" />
|
||||
@@ -616,7 +616,7 @@ function FolderTreeItem({ folder, selectedFolderId, onSelect, onDelete, onLink,
|
||||
{/* Read-only indicator for external folders */}
|
||||
{isExternal && folder.external_readonly && (
|
||||
<span title={t('fileManager.readOnly')}>
|
||||
<Lock className="w-3 h-3 text-amber-400 flex-shrink-0" />
|
||||
<Lock className="w-3 h-3 text-amber-600 dark:text-amber-400 flex-shrink-0" />
|
||||
</span>
|
||||
)}
|
||||
{folder.file_count > 0 && (
|
||||
@@ -668,7 +668,7 @@ function FolderTreeItem({ folder, selectedFolderId, onSelect, onDelete, onLink,
|
||||
</button>
|
||||
<button
|
||||
className={`w-full px-3 py-1.5 text-left text-sm flex items-center gap-2 ${
|
||||
hasPermission('library:delete_all') ? 'text-red-400 hover:bg-bambu-dark' : 'text-bambu-gray cursor-not-allowed'
|
||||
hasPermission('library:delete_all') ? 'text-red-700 dark:text-red-400 hover:bg-bambu-dark' : 'text-bambu-gray cursor-not-allowed'
|
||||
}`}
|
||||
onClick={() => { if (hasPermission('library:delete_all')) { onDelete(folder.id); setShowActions(false); } }}
|
||||
disabled={!hasPermission('library:delete_all')}
|
||||
@@ -932,7 +932,7 @@ function FileCard({ file, isSelected, isMobile, onSelect, onDelete, onDownload,
|
||||
)}
|
||||
<button
|
||||
className={`w-full px-3 py-1.5 text-left text-sm flex items-center gap-2 ${
|
||||
canModify('library', 'delete', file.created_by_id) ? 'text-red-400 hover:bg-bambu-dark' : 'text-bambu-gray cursor-not-allowed'
|
||||
canModify('library', 'delete', file.created_by_id) ? 'text-red-700 dark:text-red-400 hover:bg-bambu-dark' : 'text-bambu-gray cursor-not-allowed'
|
||||
}`}
|
||||
onClick={() => { if (canModify('library', 'delete', file.created_by_id)) { onDelete(file.id); setShowActions(false); } }}
|
||||
disabled={!canModify('library', 'delete', file.created_by_id)}
|
||||
@@ -1723,12 +1723,12 @@ export function FileManagerPage() {
|
||||
<span className="text-white font-medium">{stats.total_files}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<FolderOpen className="w-4 h-4 text-blue-400" />
|
||||
<FolderOpen className="w-4 h-4 text-blue-600 dark:text-blue-400" />
|
||||
<span className="text-bambu-gray">{t('fileManager.folders')}:</span>
|
||||
<span className="text-white font-medium">{stats.total_folders}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<HardDrive className="w-4 h-4 text-amber-400" />
|
||||
<HardDrive className="w-4 h-4 text-amber-600 dark:text-amber-400" />
|
||||
<span className="text-bambu-gray">{t('fileManager.size')}:</span>
|
||||
<span className="text-white font-medium">{formatFileSize(stats.total_size_bytes)}</span>
|
||||
</div>
|
||||
@@ -1909,7 +1909,7 @@ export function FileManagerPage() {
|
||||
setTopLevelView('external');
|
||||
}}
|
||||
>
|
||||
<FolderSymlink className="w-4 h-4 text-purple-400" />
|
||||
<FolderSymlink className="w-4 h-4 text-purple-600 dark:text-purple-400" />
|
||||
<span className="text-sm">{t('fileManager.allExternal')}</span>
|
||||
</div>
|
||||
)}
|
||||
@@ -1983,13 +1983,13 @@ export function FileManagerPage() {
|
||||
)}
|
||||
{/* External folder info bar */}
|
||||
{selectedFolder?.is_external && (
|
||||
<div className="flex items-center gap-3 mb-4 p-3 bg-purple-500/10 border border-purple-500/30 rounded-lg">
|
||||
<FolderSymlink className="w-5 h-5 text-purple-400 flex-shrink-0" />
|
||||
<div className="flex items-center gap-3 mb-4 p-3 bg-purple-50 dark:bg-purple-500/10 border border-purple-300 dark:border-purple-500/30 rounded-lg">
|
||||
<FolderSymlink className="w-5 h-5 text-purple-600 dark:text-purple-400 flex-shrink-0" />
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-medium text-purple-300">{t('fileManager.externalFolder')}</span>
|
||||
<span className="text-sm font-medium text-purple-700 dark:text-purple-300">{t('fileManager.externalFolder')}</span>
|
||||
{selectedFolder.external_readonly && (
|
||||
<span className="text-xs px-1.5 py-0.5 rounded bg-amber-500/20 text-amber-400 flex items-center gap-1">
|
||||
<span className="text-xs px-1.5 py-0.5 rounded bg-amber-100 dark:bg-amber-500/20 text-amber-700 dark:text-amber-400 flex items-center gap-1">
|
||||
<Lock className="w-3 h-3" />
|
||||
{t('fileManager.readOnly')}
|
||||
</span>
|
||||
@@ -2398,8 +2398,8 @@ export function FileManagerPage() {
|
||||
<div>
|
||||
<span className={`text-xs px-1.5 py-0.5 rounded font-medium ${
|
||||
file.file_type === '3mf' ? 'bg-bambu-green/20 text-bambu-green'
|
||||
: (file.file_type === 'gcode' || file.file_type === 'gcode.3mf') ? 'bg-blue-500/20 text-blue-400'
|
||||
: file.file_type === 'stl' ? 'bg-purple-500/20 text-purple-400'
|
||||
: (file.file_type === 'gcode' || file.file_type === 'gcode.3mf') ? 'bg-blue-100 dark:bg-blue-500/20 text-blue-700 dark:text-blue-400'
|
||||
: file.file_type === 'stl' ? 'bg-purple-100 dark:bg-purple-500/20 text-purple-700 dark:text-purple-400'
|
||||
: 'bg-bambu-gray/20 text-bambu-gray'
|
||||
}`}>
|
||||
{file.file_type.toUpperCase()}
|
||||
@@ -2542,7 +2542,7 @@ export function FileManagerPage() {
|
||||
onClick={() => canModify('library', 'delete', file.created_by_id) && setDeleteConfirm({ type: 'file', id: file.id })}
|
||||
className={`p-1.5 rounded transition-colors ${
|
||||
canModify('library', 'delete', file.created_by_id)
|
||||
? 'hover:bg-bambu-dark text-bambu-gray hover:text-red-400'
|
||||
? 'hover:bg-bambu-dark text-bambu-gray hover:text-red-700 dark:hover:text-red-400'
|
||||
: 'text-bambu-gray/50 cursor-not-allowed'
|
||||
}`}
|
||||
title={canModify('library', 'delete', file.created_by_id) ? t('common.delete') : t('fileManager.noPermissionDeleteFile')}
|
||||
|
||||
@@ -176,7 +176,7 @@ export function GroupEditPage() {
|
||||
|
||||
{/* System group warning */}
|
||||
{isEditing && groupData?.is_system && (
|
||||
<div className="flex items-center gap-3 px-4 py-3 rounded-lg bg-yellow-500/10 border border-yellow-500/20 text-yellow-400 text-sm">
|
||||
<div className="flex items-center gap-3 px-4 py-3 rounded-lg bg-yellow-50 dark:bg-yellow-500/10 border border-yellow-300 dark:border-yellow-500/20 text-yellow-700 dark:text-yellow-400 text-sm">
|
||||
<AlertTriangle className="w-4 h-4 shrink-0" />
|
||||
{t('groups.form.systemGroupWarning')}
|
||||
</div>
|
||||
|
||||
@@ -132,14 +132,14 @@ function formatWeight(g: number, useKg = false): string {
|
||||
|
||||
// Material color mapping for pills
|
||||
const MATERIAL_COLORS: Record<string, string> = {
|
||||
PLA: 'bg-green-500/20 text-green-400',
|
||||
ABS: 'bg-red-500/20 text-red-400',
|
||||
PETG: 'bg-blue-500/20 text-blue-400',
|
||||
TPU: 'bg-purple-500/20 text-purple-400',
|
||||
ASA: 'bg-orange-500/20 text-orange-400',
|
||||
PA: 'bg-yellow-500/20 text-yellow-400',
|
||||
PC: 'bg-cyan-500/20 text-cyan-400',
|
||||
PET: 'bg-sky-500/20 text-sky-400',
|
||||
PLA: 'bg-green-100 dark:bg-green-500/20 text-green-700 dark:text-green-400',
|
||||
ABS: 'bg-red-100 dark:bg-red-500/20 text-red-700 dark:text-red-400',
|
||||
PETG: 'bg-blue-100 dark:bg-blue-500/20 text-blue-700 dark:text-blue-400',
|
||||
TPU: 'bg-purple-100 dark:bg-purple-500/20 text-purple-700 dark:text-purple-400',
|
||||
ASA: 'bg-orange-100 dark:bg-orange-500/20 text-orange-700 dark:text-orange-400',
|
||||
PA: 'bg-yellow-100 dark:bg-yellow-500/20 text-yellow-700 dark:text-yellow-400',
|
||||
PC: 'bg-cyan-100 dark:bg-cyan-500/20 text-cyan-700 dark:text-cyan-400',
|
||||
PET: 'bg-sky-100 dark:bg-sky-500/20 text-sky-700 dark:text-sky-400',
|
||||
};
|
||||
|
||||
type TFn = (key: string, opts?: Record<string, unknown>) => string;
|
||||
@@ -257,7 +257,7 @@ const columnCells: Record<string, (ctx: CellCtx) => ReactNode> = {
|
||||
const isHt = !isExternal && assignment.ams_id >= 128;
|
||||
const slotLabel = formatSlotLabel(assignment.ams_id, assignment.tray_id, isHt, isExternal);
|
||||
return (
|
||||
<span className="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-purple-500/20 text-purple-400">
|
||||
<span className="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-purple-100 dark:bg-purple-500/20 text-purple-700 dark:text-purple-400">
|
||||
{printerLabel} {slotLabel}{assignment.ams_label ? ` (${assignment.ams_label})` : ''}
|
||||
</span>
|
||||
);
|
||||
@@ -265,7 +265,7 @@ const columnCells: Record<string, (ctx: CellCtx) => ReactNode> = {
|
||||
storage_location: ({ spool }) => {
|
||||
if (!spool.storage_location) return <span className="text-sm text-bambu-gray">-</span>;
|
||||
return (
|
||||
<span className="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-blue-500/20 text-blue-400">
|
||||
<span className="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-blue-100 dark:bg-blue-500/20 text-blue-700 dark:text-blue-400">
|
||||
{spool.storage_location}
|
||||
</span>
|
||||
);
|
||||
@@ -321,7 +321,7 @@ const columnCells: Record<string, (ctx: CellCtx) => ReactNode> = {
|
||||
stock: ({ spool, t }) => {
|
||||
if (!spool.slicer_filament) {
|
||||
return (
|
||||
<span className="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-amber-500/20 text-amber-400">
|
||||
<span className="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-amber-100 dark:bg-amber-500/20 text-amber-700 dark:text-amber-400">
|
||||
{t('inventory.stock')}
|
||||
</span>
|
||||
);
|
||||
@@ -373,7 +373,7 @@ const columnCells: Record<string, (ctx: CellCtx) => ReactNode> = {
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`flex items-center gap-1 text-sm font-medium ${isMatch ? 'text-green-400' : 'text-yellow-400'}`}
|
||||
className={`flex items-center gap-1 text-sm font-medium ${isMatch ? 'text-green-700 dark:text-green-400' : 'text-yellow-700 dark:text-yellow-400'}`}
|
||||
title={tooltip}
|
||||
>
|
||||
<span>{Math.round(scaleWeight)}g</span>
|
||||
@@ -1380,13 +1380,13 @@ function InventoryPage({ spoolmanMode = false, spoolmanModeReady = true }: { spo
|
||||
<div className="bg-bambu-dark-secondary rounded-lg p-4">
|
||||
<div className="flex items-center justify-between gap-2 mb-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<TrendingDown className="w-4 h-4 text-blue-400" />
|
||||
<TrendingDown className="w-4 h-4 text-blue-600 dark:text-blue-400" />
|
||||
<span className="text-xs text-bambu-gray font-medium uppercase tracking-wide">{t('inventory.totalConsumed')}</span>
|
||||
</div>
|
||||
{stats.totalConsumed > 0 && resetableSpoolIds.length > 0 && (
|
||||
<button
|
||||
onClick={() => setConfirmAction({ type: 'reset-all-consumed-counters' })}
|
||||
className="p-1 text-bambu-gray hover:text-red-400 rounded transition-colors"
|
||||
className="p-1 text-bambu-gray hover:text-red-600 dark:hover:text-red-400 rounded transition-colors"
|
||||
title={t('inventory.resetAllConsumedCountersTooltip')}
|
||||
aria-label={t('inventory.resetAllConsumedCounters')}
|
||||
>
|
||||
@@ -1401,7 +1401,7 @@ function InventoryPage({ spoolmanMode = false, spoolmanModeReady = true }: { spo
|
||||
{/* By Material */}
|
||||
<div className="bg-bambu-dark-secondary rounded-lg p-4">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<Layers className="w-4 h-4 text-green-400" />
|
||||
<Layers className="w-4 h-4 text-green-600 dark:text-green-400" />
|
||||
<span className="text-xs text-bambu-gray font-medium uppercase tracking-wide">{t('inventory.byMaterial')}</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-1.5 mt-1">
|
||||
@@ -1419,7 +1419,7 @@ function InventoryPage({ spoolmanMode = false, spoolmanModeReady = true }: { spo
|
||||
{/* In Printer */}
|
||||
<div className="bg-bambu-dark-secondary rounded-lg p-4">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<Printer className="w-4 h-4 text-purple-400" />
|
||||
<Printer className="w-4 h-4 text-purple-600 dark:text-purple-400" />
|
||||
<span className="text-xs text-bambu-gray font-medium uppercase tracking-wide">{t('inventory.inPrinter')}</span>
|
||||
</div>
|
||||
<div className="text-xl font-bold text-white">{inPrinterCount}</div>
|
||||
@@ -1429,10 +1429,10 @@ function InventoryPage({ spoolmanMode = false, spoolmanModeReady = true }: { spo
|
||||
{/* Low Stock */}
|
||||
<div className="bg-bambu-dark-secondary rounded-lg p-4">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<AlertTriangle className="w-4 h-4 text-yellow-400" />
|
||||
<AlertTriangle className="w-4 h-4 text-yellow-600 dark:text-yellow-400" />
|
||||
<span className="text-xs text-bambu-gray font-medium uppercase tracking-wide">{t('inventory.lowStock')}</span>
|
||||
</div>
|
||||
<div className={`text-xl font-bold ${stats.lowStock > 0 ? 'text-yellow-400' : 'text-white'}`}>{stats.lowStock}</div>
|
||||
<div className={`text-xl font-bold ${stats.lowStock > 0 ? 'text-yellow-700 dark:text-yellow-400' : 'text-white'}`}>{stats.lowStock}</div>
|
||||
<div className="text-xs text-bambu-gray mt-1 flex items-center gap-2">
|
||||
{showThresholdInput ? (
|
||||
<form
|
||||
@@ -1646,7 +1646,7 @@ function InventoryPage({ spoolmanMode = false, spoolmanModeReady = true }: { spo
|
||||
onClick={() => { setUsageFilter('lowstock'); resetPage(); }}
|
||||
className={`flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium transition-colors ${
|
||||
usageFilter === 'lowstock'
|
||||
? 'bg-yellow-500/20 text-yellow-400'
|
||||
? 'bg-yellow-100 dark:bg-yellow-500/20 text-yellow-700 dark:text-yellow-400'
|
||||
: 'text-bambu-gray hover:bg-bambu-dark-tertiary'
|
||||
}`}
|
||||
>
|
||||
@@ -1671,7 +1671,7 @@ function InventoryPage({ spoolmanMode = false, spoolmanModeReady = true }: { spo
|
||||
onClick={() => { setStockFilter('stock'); resetPage(); }}
|
||||
className={`px-3 py-1.5 text-xs font-medium transition-colors ${
|
||||
stockFilter === 'stock'
|
||||
? 'bg-amber-500/20 text-amber-400'
|
||||
? 'bg-amber-100 dark:bg-amber-500/20 text-amber-700 dark:text-amber-400'
|
||||
: 'text-bambu-gray hover:bg-bambu-dark-tertiary'
|
||||
}`}
|
||||
>
|
||||
@@ -2582,7 +2582,7 @@ function SpoolTableRow({
|
||||
// archived consumed weight now counts in "Total Consumed", so
|
||||
// the user needs a way to zero an archived spool's tracking
|
||||
// counter individually without having to un-archive it first.
|
||||
<button onClick={onResetConsumedCounter} className="p-1.5 text-bambu-gray hover:text-orange-400 rounded transition-colors" title={t('inventory.resetConsumedCounterTooltip')}>
|
||||
<button onClick={onResetConsumedCounter} className="p-1.5 text-bambu-gray hover:text-orange-600 dark:hover:text-orange-400 rounded transition-colors" title={t('inventory.resetConsumedCounterTooltip')}>
|
||||
<Eraser className="w-4 h-4" />
|
||||
</button>
|
||||
)}
|
||||
@@ -2591,11 +2591,11 @@ function SpoolTableRow({
|
||||
<RotateCcw className="w-4 h-4" />
|
||||
</button>
|
||||
) : (
|
||||
<button onClick={onArchive} className="p-1.5 text-bambu-gray hover:text-yellow-400 rounded transition-colors" title={t('inventory.archive')}>
|
||||
<button onClick={onArchive} className="p-1.5 text-bambu-gray hover:text-yellow-600 dark:hover:text-yellow-400 rounded transition-colors" title={t('inventory.archive')}>
|
||||
<Archive className="w-4 h-4" />
|
||||
</button>
|
||||
)}
|
||||
<button onClick={onDelete} className="p-1.5 text-bambu-gray hover:text-red-400 rounded transition-colors" title={t('common.delete')}>
|
||||
<button onClick={onDelete} className="p-1.5 text-bambu-gray hover:text-red-600 dark:hover:text-red-400 rounded transition-colors" title={t('common.delete')}>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -704,7 +704,7 @@ export function LoginPage() {
|
||||
</div>
|
||||
|
||||
{showAutologinBanner && (
|
||||
<div className="mt-6 rounded-lg border border-amber-500/40 bg-amber-500/10 px-4 py-3 text-sm text-amber-200">
|
||||
<div className="mt-6 rounded-lg border border-amber-300 dark:border-amber-500/40 bg-amber-50 dark:bg-amber-500/10 px-4 py-3 text-sm text-amber-800 dark:text-amber-200">
|
||||
{t('login.autologinFailed')}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -159,8 +159,8 @@ function MaintenanceCard({
|
||||
|
||||
const getStatusColor = () => {
|
||||
if (!item.enabled) return 'text-bambu-gray';
|
||||
if (item.is_due) return 'text-red-400';
|
||||
if (item.is_warning) return 'text-amber-400';
|
||||
if (item.is_due) return 'text-red-700 dark:text-red-400';
|
||||
if (item.is_warning) return 'text-amber-700 dark:text-amber-400';
|
||||
return 'text-bambu-green';
|
||||
};
|
||||
|
||||
@@ -333,13 +333,13 @@ function PrinterSection({
|
||||
<h2 className="text-xl font-semibold text-white">{overview.printer_name}</h2>
|
||||
<div className="flex items-center gap-2">
|
||||
{overview.due_count > 0 && (
|
||||
<span className="px-2.5 py-1 bg-red-500/20 text-red-400 text-xs font-medium rounded-full flex items-center gap-1.5">
|
||||
<span className="px-2.5 py-1 bg-red-100 text-red-700 dark:bg-red-500/20 dark:text-red-400 text-xs font-medium rounded-full flex items-center gap-1.5">
|
||||
<AlertTriangle className="w-3 h-3" />
|
||||
{t('maintenance.overdueCount', { count: overview.due_count })}
|
||||
</span>
|
||||
)}
|
||||
{overview.warning_count > 0 && (
|
||||
<span className="px-2.5 py-1 bg-amber-500/20 text-amber-400 text-xs font-medium rounded-full flex items-center gap-1.5">
|
||||
<span className="px-2.5 py-1 bg-amber-100 text-amber-700 dark:bg-amber-500/20 dark:text-amber-400 text-xs font-medium rounded-full flex items-center gap-1.5">
|
||||
<Clock className="w-3 h-3" />
|
||||
{t('maintenance.dueSoonCount', { count: overview.warning_count })}
|
||||
</span>
|
||||
@@ -417,14 +417,14 @@ function PrinterSection({
|
||||
}`}>
|
||||
{(() => {
|
||||
const Icon = getIcon(nextTask.maintenance_type_icon);
|
||||
return <Icon className={`w-4 h-4 ${nextTask.is_due ? 'text-red-400' : 'text-amber-400'}`} />;
|
||||
return <Icon className={`w-4 h-4 ${nextTask.is_due ? 'text-red-600 dark:text-red-400' : 'text-amber-600 dark:text-amber-400'}`} />;
|
||||
})()}
|
||||
</div>
|
||||
<div>
|
||||
<div className={`text-sm font-medium ${nextTask.is_due ? 'text-red-400' : 'text-amber-400'}`}>
|
||||
<div className={`text-sm font-medium ${nextTask.is_due ? 'text-red-700 dark:text-red-400' : 'text-amber-700 dark:text-amber-400'}`}>
|
||||
{nextTask.maintenance_type_name}
|
||||
</div>
|
||||
<div className={`text-xs ${nextTask.is_due ? 'text-red-400/70' : 'text-amber-400/70'}`}>
|
||||
<div className={`text-xs ${nextTask.is_due ? 'text-red-700/80 dark:text-red-400/70' : 'text-amber-700/80 dark:text-amber-400/70'}`}>
|
||||
{nextTask.is_due ? t('common.overdue') : t('maintenance.dueSoon')}
|
||||
</div>
|
||||
</div>
|
||||
@@ -736,7 +736,7 @@ function SettingsSection({
|
||||
))}
|
||||
</div>
|
||||
{selectedPrinters.size === 0 && (
|
||||
<p className="text-xs text-orange-400 mt-1">{t('maintenance.selectAtLeastOnePrinter')}</p>
|
||||
<p className="text-xs text-orange-700 dark:text-orange-400 mt-1">{t('maintenance.selectAtLeastOnePrinter')}</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-4 flex justify-end gap-2">
|
||||
@@ -778,7 +778,7 @@ function SettingsSection({
|
||||
}}
|
||||
disabled={!hasPermission('maintenance:delete')}
|
||||
title={!hasPermission('maintenance:delete') ? t('maintenance.noPermissionDeleteTypes') : undefined}
|
||||
className={`p-2 rounded-lg hover:bg-bambu-dark text-bambu-gray hover:text-red-400 transition-colors ${!hasPermission('maintenance:delete') ? 'opacity-50 cursor-not-allowed' : ''}`}
|
||||
className={`p-2 rounded-lg hover:bg-bambu-dark text-bambu-gray hover:text-red-600 dark:hover:text-red-400 transition-colors ${!hasPermission('maintenance:delete') ? 'opacity-50 cursor-not-allowed' : ''}`}
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</button>
|
||||
@@ -887,7 +887,7 @@ function SettingsSection({
|
||||
className={`px-2 py-1 rounded-lg border transition-colors flex items-center gap-1 ${
|
||||
assignedPrinters.length > 0
|
||||
? 'border-bambu-green/50 bg-bambu-green/10 text-bambu-green hover:bg-bambu-green/20'
|
||||
: 'border-orange-400/50 bg-orange-400/10 text-orange-400 hover:bg-orange-400/20'
|
||||
: 'border-orange-300 bg-orange-50 text-orange-700 hover:bg-orange-100 dark:border-orange-400/50 dark:bg-orange-400/10 dark:text-orange-400 dark:hover:bg-orange-400/20'
|
||||
}`}
|
||||
title={t('maintenance.printersAssignedClick', { count: assignedPrinters.length })}
|
||||
>
|
||||
@@ -911,7 +911,7 @@ function SettingsSection({
|
||||
}}
|
||||
disabled={!hasPermission('maintenance:delete')}
|
||||
title={!hasPermission('maintenance:delete') ? t('maintenance.noPermissionDeleteTypes') : undefined}
|
||||
className={`p-2 rounded-lg hover:bg-bambu-dark text-bambu-gray hover:text-red-400 transition-colors ${!hasPermission('maintenance:delete') ? 'opacity-50 cursor-not-allowed' : ''}`}
|
||||
className={`p-2 rounded-lg hover:bg-bambu-dark text-bambu-gray hover:text-red-600 dark:hover:text-red-400 transition-colors ${!hasPermission('maintenance:delete') ? 'opacity-50 cursor-not-allowed' : ''}`}
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</button>
|
||||
@@ -922,7 +922,7 @@ function SettingsSection({
|
||||
<div className="mt-3 pt-3 border-t border-bambu-dark-tertiary">
|
||||
<p className="text-xs text-bambu-gray mb-2">{t('maintenance.assignedToPrinters')}</p>
|
||||
{assignedPrinters.length === 0 ? (
|
||||
<p className="text-xs text-orange-400">{t('maintenance.noPrintersAssigned')}</p>
|
||||
<p className="text-xs text-orange-700 dark:text-orange-400">{t('maintenance.noPrintersAssigned')}</p>
|
||||
) : (
|
||||
<div className="flex flex-wrap gap-1 mb-2">
|
||||
{assignedPrinters.map(p => (
|
||||
@@ -935,7 +935,7 @@ function SettingsSection({
|
||||
onClick={() => p.itemId && onRemoveItem(p.itemId)}
|
||||
disabled={!hasPermission('maintenance:delete')}
|
||||
title={!hasPermission('maintenance:delete') ? t('maintenance.noPermissionRemovePrinter') : t('maintenance.removeFromPrinter')}
|
||||
className={`ml-1 ${hasPermission('maintenance:delete') ? 'hover:text-red-400' : 'opacity-50 cursor-not-allowed'}`}
|
||||
className={`ml-1 ${hasPermission('maintenance:delete') ? 'hover:text-red-600 dark:hover:text-red-400' : 'opacity-50 cursor-not-allowed'}`}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
@@ -1238,9 +1238,9 @@ export function MaintenancePage() {
|
||||
<p className="text-bambu-gray mt-1">
|
||||
{activeTab === 'status' ? (
|
||||
<>
|
||||
{totalDue > 0 && <span className="text-red-400">{t('maintenance.dueCount', { count: totalDue })}</span>}
|
||||
{totalDue > 0 && <span className="text-red-700 dark:text-red-400">{t('maintenance.dueCount', { count: totalDue })}</span>}
|
||||
{totalDue > 0 && totalWarning > 0 && ' · '}
|
||||
{totalWarning > 0 && <span className="text-amber-400">{t('maintenance.warningCount', { count: totalWarning })}</span>}
|
||||
{totalWarning > 0 && <span className="text-amber-700 dark:text-amber-400">{t('maintenance.warningCount', { count: totalWarning })}</span>}
|
||||
{totalDue === 0 && totalWarning === 0 && <span className="text-bambu-green">{t('maintenance.allOk')}</span>}
|
||||
</>
|
||||
) : (
|
||||
|
||||
@@ -102,11 +102,11 @@ export function NotificationsPage() {
|
||||
</div>
|
||||
|
||||
{/* Info card */}
|
||||
<Card className="mb-6 border-blue-500/30 bg-blue-500/5">
|
||||
<Card className="mb-6 border-blue-300 bg-blue-50 dark:border-blue-500/30 dark:bg-blue-500/5">
|
||||
<CardContent className="py-4">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="w-10 h-10 rounded-full flex items-center justify-center bg-blue-500/20 flex-shrink-0">
|
||||
<Mail className="w-5 h-5 text-blue-400" />
|
||||
<div className="w-10 h-10 rounded-full flex items-center justify-center bg-blue-100 dark:bg-blue-500/20 flex-shrink-0">
|
||||
<Mail className="w-5 h-5 text-blue-600 dark:text-blue-400" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-white font-medium">{t('notifications.userEmail.emailNotifications')}</h3>
|
||||
@@ -114,11 +114,11 @@ export function NotificationsPage() {
|
||||
{t('notifications.userEmail.emailNotificationsDesc')}
|
||||
</p>
|
||||
{user?.email ? (
|
||||
<p className="text-sm text-blue-400 mt-2">
|
||||
<p className="text-sm text-blue-700 dark:text-blue-400 mt-2">
|
||||
{t('notifications.userEmail.sendingTo')}: <strong>{user.email}</strong>
|
||||
</p>
|
||||
) : (
|
||||
<p className="text-sm text-yellow-400 mt-2">
|
||||
<p className="text-sm text-yellow-700 dark:text-yellow-400 mt-2">
|
||||
{t('notifications.userEmail.noEmailWarning')}
|
||||
</p>
|
||||
)}
|
||||
|
||||
@@ -268,7 +268,7 @@ export function PipelineRunsView() {
|
||||
type="button"
|
||||
onClick={() => setShowClearConfirm(true)}
|
||||
disabled={total === 0}
|
||||
className="flex items-center gap-1 px-2 py-1 text-red-400 hover:bg-red-500/10 rounded disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
className="flex items-center gap-1 px-2 py-1 text-red-700 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-500/10 rounded disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
<Trash2 className="w-3 h-3" />
|
||||
{t('pipelineRuns.clearLog', 'Clear log')}
|
||||
@@ -291,7 +291,7 @@ export function PipelineRunsView() {
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<h3 className="text-base font-semibold text-white flex items-center gap-2">
|
||||
<Trash2 className="w-4 h-4 text-red-400" />
|
||||
<Trash2 className="w-4 h-4 text-red-600 dark:text-red-400" />
|
||||
{t('pipelineRuns.clearConfirmTitle', 'Clear log?')}
|
||||
</h3>
|
||||
<p className="text-sm text-bambu-gray mt-2">
|
||||
@@ -490,7 +490,7 @@ function RunRow({
|
||||
{run.copies_failed > 0 && (
|
||||
<>
|
||||
<span className="text-bambu-gray/40">·</span>
|
||||
<span className="text-red-400">
|
||||
<span className="text-red-700 dark:text-red-400">
|
||||
{t('pipelineRuns.failedCount', '{{n}} failed', { n: run.copies_failed })}
|
||||
</span>
|
||||
</>
|
||||
@@ -506,7 +506,7 @@ function RunRow({
|
||||
onClick={onCancel}
|
||||
disabled={cancelling}
|
||||
aria-label={t('common.cancel', 'Cancel')}
|
||||
className="text-xs px-2 py-1 text-red-400 hover:bg-red-500/10 rounded disabled:opacity-50 flex items-center gap-1"
|
||||
className="text-xs px-2 py-1 text-red-700 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-500/10 rounded disabled:opacity-50 flex items-center gap-1"
|
||||
>
|
||||
<X className="w-3 h-3" />
|
||||
{t('common.cancel', 'Cancel')}
|
||||
@@ -542,7 +542,7 @@ function RunRow({
|
||||
</span>
|
||||
)}
|
||||
{job.error_message && (
|
||||
<span className="text-red-400 truncate" title={job.error_message}>
|
||||
<span className="text-red-700 dark:text-red-400 truncate" title={job.error_message}>
|
||||
{job.error_message}
|
||||
</span>
|
||||
)}
|
||||
@@ -550,7 +550,7 @@ function RunRow({
|
||||
))}
|
||||
</div>
|
||||
{run.error_message && !userCancelled && (
|
||||
<div className="text-xs text-red-400 mt-2 pt-2 border-t border-bambu-dark-tertiary">
|
||||
<div className="text-xs text-red-700 dark:text-red-400 mt-2 pt-2 border-t border-bambu-dark-tertiary">
|
||||
{run.error_message}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -412,7 +412,7 @@ function DualNozzleHoverCard({ leftSlot, rightSlot, activeNozzle, filamentInfo,
|
||||
const filamentName = slot.filament_id ? filamentInfo?.[slot.filament_id]?.name : undefined;
|
||||
return (
|
||||
<div className="flex-1 space-y-1.5">
|
||||
<div className={`text-[10px] font-bold pb-1 border-b border-bambu-dark-tertiary/50 ${isActive ? 'text-amber-400' : 'text-bambu-gray'}`}>
|
||||
<div className={`text-[10px] font-bold pb-1 border-b border-bambu-dark-tertiary/50 ${isActive ? 'text-amber-700 dark:text-amber-400' : 'text-bambu-gray'}`}>
|
||||
{side === 'L' ? t('common.left') : t('common.right')}
|
||||
</div>
|
||||
{slot.nozzle_diameter && (
|
||||
@@ -716,8 +716,8 @@ function AmsBackupBadge({ state, onClick }: AmsBackupBadgeProps) {
|
||||
let title: string;
|
||||
if (state === true) {
|
||||
className += known
|
||||
? 'bg-blue-500/20 text-blue-400 hover:bg-blue-500/30 cursor-pointer'
|
||||
: 'bg-blue-500/20 text-blue-400 cursor-default';
|
||||
? 'bg-blue-100 dark:bg-blue-500/20 text-blue-700 dark:text-blue-400 hover:bg-blue-500/30 cursor-pointer'
|
||||
: 'bg-blue-100 dark:bg-blue-500/20 text-blue-700 dark:text-blue-400 cursor-default';
|
||||
title = t('printers.amsBackup.titleOn');
|
||||
} else if (state === false) {
|
||||
className += known
|
||||
@@ -1347,9 +1347,9 @@ function NozzleTemperatureControlBox({
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`rounded-lg border p-2 ${isActive ? 'border-amber-400/60 bg-amber-400/10' : 'border-bambu-dark-tertiary bg-bambu-dark'}`}>
|
||||
<div className={`rounded-lg border p-2 ${isActive ? 'border-amber-500/60 dark:border-amber-400/60 bg-amber-50 dark:bg-amber-400/10' : 'border-bambu-dark-tertiary bg-bambu-dark'}`}>
|
||||
<div className="mb-1.5 flex items-center justify-between gap-2">
|
||||
<span className={`text-xs font-medium ${isActive ? 'text-amber-300' : 'text-white'}`}>{label}</span>
|
||||
<span className={`text-xs font-medium ${isActive ? 'text-amber-700 dark:text-amber-300' : 'text-white'}`}>{label}</span>
|
||||
<span className="text-[10px] text-bambu-gray">
|
||||
{Math.round(current ?? 0)}°C
|
||||
{target !== undefined ? ` / ${Math.round(target)}°` : ''}
|
||||
@@ -1682,7 +1682,7 @@ export function AmsNameHoverCard({
|
||||
{canEdit && (
|
||||
<div className="space-y-1">
|
||||
{saveError && (
|
||||
<p className="text-[10px] text-red-400 break-words">{saveError}</p>
|
||||
<p className="text-[10px] text-red-700 dark:text-red-400 break-words">{saveError}</p>
|
||||
)}
|
||||
<div className="flex gap-1 justify-end">
|
||||
<button
|
||||
@@ -2140,13 +2140,13 @@ function PrinterCard({
|
||||
if (isPrintingOrPaused) {
|
||||
return {
|
||||
label: t('printers.plateStatus.inUse'),
|
||||
className: 'bg-blue-500/20 text-blue-400',
|
||||
className: 'bg-blue-100 dark:bg-blue-500/20 text-blue-700 dark:text-blue-400',
|
||||
};
|
||||
}
|
||||
if (status.awaiting_plate_clear) {
|
||||
return {
|
||||
label: t('printers.plateStatus.notCleared'),
|
||||
className: 'bg-yellow-500/20 text-yellow-400',
|
||||
className: 'bg-yellow-100 dark:bg-yellow-500/20 text-yellow-700 dark:text-yellow-400',
|
||||
};
|
||||
}
|
||||
return {
|
||||
@@ -3089,8 +3089,8 @@ function PrinterCard({
|
||||
<button
|
||||
className={`w-full px-4 py-2 text-left text-sm flex items-center gap-2 ${
|
||||
hasPermission('printers:delete')
|
||||
? 'text-red-400 hover:bg-bambu-dark-tertiary'
|
||||
: 'text-red-400/50 cursor-not-allowed'
|
||||
? 'text-red-700 dark:text-red-400 hover:bg-bambu-dark-tertiary'
|
||||
: 'text-red-700/50 dark:text-red-400/50 cursor-not-allowed'
|
||||
}`}
|
||||
onClick={() => {
|
||||
if (!hasPermission('printers:delete')) return;
|
||||
@@ -3138,7 +3138,7 @@ function PrinterCard({
|
||||
? 'bg-bambu-green/10 border-bambu-green/50'
|
||||
: canDrop
|
||||
? 'bg-bambu-green/10 border-bambu-green'
|
||||
: 'bg-red-500/10 border-red-500/50'
|
||||
: 'bg-red-50 dark:bg-red-500/10 border-red-300 dark:border-red-500/50'
|
||||
}`}
|
||||
>
|
||||
<div className="text-center">
|
||||
@@ -3154,8 +3154,8 @@ function PrinterCard({
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<X className="w-8 h-8 mx-auto mb-2 text-red-400" />
|
||||
<p className="text-sm font-medium text-red-400">{t('printers.cannotPrint', 'Printer busy')}</p>
|
||||
<X className="w-8 h-8 mx-auto mb-2 text-red-600 dark:text-red-400" />
|
||||
<p className="text-sm font-medium text-red-700 dark:text-red-400">{t('printers.cannotPrint', 'Printer busy')}</p>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
@@ -3208,7 +3208,7 @@ function PrinterCard({
|
||||
onClick={() => clearPlateMutation.mutate()}
|
||||
disabled={clearPlateMutation.isPending || !hasPermission('printers:clear_plate')}
|
||||
aria-label={t('printers.plateStatus.markCleared')}
|
||||
className="inline-flex h-5 w-5 flex-shrink-0 items-center justify-center rounded-md bg-yellow-500/20 border border-yellow-400/40 text-yellow-400 hover:bg-yellow-500/30 transition-colors disabled:opacity-50"
|
||||
className="inline-flex h-5 w-5 flex-shrink-0 items-center justify-center rounded-md bg-yellow-100 dark:bg-yellow-500/20 border border-yellow-300 dark:border-yellow-400/40 text-yellow-700 dark:text-yellow-400 hover:bg-yellow-500/30 transition-colors disabled:opacity-50"
|
||||
title={!hasPermission('printers:clear_plate') ? t('printers.permission.noControl') : t('printers.plateStatus.markCleared')}
|
||||
>
|
||||
{clearPlateMutation.isPending ? (
|
||||
@@ -3250,7 +3250,7 @@ function PrinterCard({
|
||||
maintenance UI. */}
|
||||
{printer.is_active === false ? (
|
||||
<span
|
||||
className="flex items-center gap-1.5 px-2 py-1 rounded-full text-xs bg-amber-500/20 text-amber-400"
|
||||
className="flex items-center gap-1.5 px-2 py-1 rounded-full text-xs bg-amber-100 dark:bg-amber-500/20 text-amber-700 dark:text-amber-400"
|
||||
title={t('printers.maintenance.subtitle')}
|
||||
>
|
||||
<Wrench className="w-3 h-3" />
|
||||
@@ -3360,7 +3360,7 @@ function PrinterCard({
|
||||
{queueCount > 0 && (
|
||||
<button
|
||||
onClick={() => navigate('/queue')}
|
||||
className="flex items-center gap-1 px-2 py-1 rounded-full text-xs bg-indigo-500/20 text-indigo-400 hover:opacity-80 transition-opacity"
|
||||
className="flex items-center gap-1 px-2 py-1 rounded-full text-xs bg-indigo-100 dark:bg-indigo-500/20 text-indigo-700 dark:text-indigo-400 hover:opacity-80 transition-opacity"
|
||||
title={t('printers.queue.inQueue', { count: queueCount })}
|
||||
>
|
||||
<Layers className="w-3 h-3" />
|
||||
@@ -3373,7 +3373,7 @@ function PrinterCard({
|
||||
onClick={() => setShowFirmwareModal(true)}
|
||||
className={`flex items-center gap-1 px-2 py-1 rounded-full text-xs hover:opacity-80 transition-opacity ${
|
||||
firmwareInfo.update_available
|
||||
? 'bg-orange-500/20 text-orange-400'
|
||||
? 'bg-orange-100 dark:bg-orange-500/20 text-orange-700 dark:text-orange-400'
|
||||
: 'bg-status-ok/20 text-status-ok'
|
||||
}`}
|
||||
title={
|
||||
@@ -3397,7 +3397,7 @@ function PrinterCard({
|
||||
<span
|
||||
className={`flex items-center px-2 py-1 rounded-full text-xs ${
|
||||
status.door_open
|
||||
? 'bg-yellow-500/20 text-yellow-400'
|
||||
? 'bg-yellow-100 dark:bg-yellow-500/20 text-yellow-700 dark:text-yellow-400'
|
||||
: 'bg-status-ok/20 text-status-ok'
|
||||
}`}
|
||||
title={status.door_open ? t('printers.door.open') : t('printers.door.closed')}
|
||||
@@ -3416,8 +3416,8 @@ function PrinterCard({
|
||||
<Card className="w-full max-w-md mx-4">
|
||||
<CardContent>
|
||||
<div className="flex items-start gap-3 mb-4">
|
||||
<div className="p-2 rounded-full bg-red-500/20">
|
||||
<AlertTriangle className="w-5 h-5 text-red-400" />
|
||||
<div className="p-2 rounded-full bg-red-100 dark:bg-red-500/20">
|
||||
<AlertTriangle className="w-5 h-5 text-red-600 dark:text-red-400" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-white">{t('printers.confirm.deleteTitle')}</h3>
|
||||
@@ -3481,9 +3481,9 @@ function PrinterCard({
|
||||
// out-of-service.
|
||||
<>
|
||||
{viewMode === 'compact' ? (
|
||||
<div className="mt-2 flex items-center gap-2 px-2 py-1.5 rounded-full bg-amber-500/15 border border-amber-500/30">
|
||||
<Wrench className="w-3 h-3 text-amber-400 shrink-0" />
|
||||
<span className="text-[11px] text-amber-400 font-medium truncate">
|
||||
<div className="mt-2 flex items-center gap-2 px-2 py-1.5 rounded-full bg-amber-50 dark:bg-amber-500/15 border border-amber-300 dark:border-amber-500/30">
|
||||
<Wrench className="w-3 h-3 text-amber-600 dark:text-amber-400 shrink-0" />
|
||||
<span className="text-[11px] text-amber-700 dark:text-amber-400 font-medium truncate">
|
||||
{t('printers.maintenance.pillLabel')}
|
||||
</span>
|
||||
</div>
|
||||
@@ -3495,10 +3495,10 @@ function PrinterCard({
|
||||
</span>
|
||||
<div className="flex-1 h-[2px] bg-bambu-dark-tertiary" />
|
||||
</div>
|
||||
<div className="p-3 bg-amber-500/10 border border-amber-500/30 rounded-[10px] flex items-center gap-3">
|
||||
<Wrench className="w-6 h-6 text-amber-400 shrink-0" />
|
||||
<div className="p-3 bg-amber-50 dark:bg-amber-500/10 border border-amber-300 dark:border-amber-500/30 rounded-[10px] flex items-center gap-3">
|
||||
<Wrench className="w-6 h-6 text-amber-600 dark:text-amber-400 shrink-0" />
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm text-amber-400 font-medium">
|
||||
<p className="text-sm text-amber-700 dark:text-amber-400 font-medium">
|
||||
{t('printers.maintenance.title')}
|
||||
</p>
|
||||
<p className="text-xs text-bambu-gray mt-0.5">
|
||||
@@ -3715,14 +3715,14 @@ function PrinterCard({
|
||||
label: t('printers.fans.partCooling'),
|
||||
value: status.cooling_fan_speed ?? 0,
|
||||
Icon: Fan,
|
||||
activeClass: 'text-cyan-400',
|
||||
activeClass: 'text-cyan-600 dark:text-cyan-400',
|
||||
},
|
||||
{
|
||||
key: 'aux',
|
||||
label: t('printers.fans.auxiliary'),
|
||||
value: status.big_fan1_speed ?? 0,
|
||||
Icon: Wind,
|
||||
activeClass: 'text-blue-400',
|
||||
activeClass: 'text-blue-600 dark:text-blue-400',
|
||||
},
|
||||
...(hasChamberFan
|
||||
? [
|
||||
@@ -3731,7 +3731,7 @@ function PrinterCard({
|
||||
label: t('printers.fans.chamber'),
|
||||
value: status.big_fan2_speed ?? 0,
|
||||
Icon: AirVent,
|
||||
activeClass: 'text-green-400',
|
||||
activeClass: 'text-green-600 dark:text-green-400',
|
||||
},
|
||||
]
|
||||
: []),
|
||||
@@ -3921,13 +3921,13 @@ function PrinterCard({
|
||||
title={canUseStatusControls ? t('printers.activeNozzle', { nozzle: activeNozzle === 'L' ? t('common.left') : t('common.right') }) : statusControlTitle}
|
||||
onClick={() => canUseStatusControls && setStatusControlMenu(statusControlMenu === 'nozzle-select' ? null : 'nozzle-select')}
|
||||
>
|
||||
<NozzleIcon className="w-3.5 h-3.5 mb-0.5 text-amber-400" />
|
||||
<NozzleIcon className="w-3.5 h-3.5 mb-0.5 text-amber-600 dark:text-amber-400" />
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={`text-[11px] font-bold ${activeNozzle === 'L' ? 'text-amber-400' : 'text-gray-500'}`}>
|
||||
<span className={`text-[11px] font-bold ${activeNozzle === 'L' ? 'text-amber-700 dark:text-amber-400' : 'text-gray-500'}`}>
|
||||
L{leftNozzleSlot?.nozzle_diameter ? ` ${leftNozzleSlot.nozzle_diameter}` : ''}
|
||||
</span>
|
||||
<span className="text-[9px] text-bambu-gray/40">·</span>
|
||||
<span className={`text-[11px] font-bold ${activeNozzle === 'R' ? 'text-amber-400' : 'text-gray-500'}`}>
|
||||
<span className={`text-[11px] font-bold ${activeNozzle === 'R' ? 'text-amber-700 dark:text-amber-400' : 'text-gray-500'}`}>
|
||||
R{rightNozzleSlot?.nozzle_diameter ? ` ${rightNozzleSlot.nozzle_diameter}` : ''}
|
||||
</span>
|
||||
</div>
|
||||
@@ -3996,7 +3996,7 @@ function PrinterCard({
|
||||
type="button"
|
||||
onClick={() => clearPlateMutation.mutate()}
|
||||
disabled={clearPlateMutation.isPending || !hasPermission('printers:clear_plate')}
|
||||
className="mt-2 w-full inline-flex items-center justify-center gap-2 px-3 py-1.5 rounded-lg bg-yellow-500/20 border border-yellow-400/40 text-yellow-400 hover:bg-yellow-500/30 transition-colors text-xs font-medium disabled:opacity-50"
|
||||
className="mt-2 w-full inline-flex items-center justify-center gap-2 px-3 py-1.5 rounded-lg bg-yellow-100 dark:bg-yellow-500/20 border border-yellow-300 dark:border-yellow-400/40 text-yellow-700 dark:text-yellow-400 hover:bg-yellow-500/30 transition-colors text-xs font-medium disabled:opacity-50"
|
||||
title={!hasPermission('printers:clear_plate') ? t('printers.permission.noControl') : t('printers.plateStatus.markCleared')}
|
||||
>
|
||||
{clearPlateMutation.isPending ? (
|
||||
@@ -4037,7 +4037,7 @@ function PrinterCard({
|
||||
disabled={!status.connected || chamberLightMutation.isPending || !hasPermission('printers:control')}
|
||||
className={`${iconControlClass} ${
|
||||
status.chamber_light
|
||||
? 'bg-yellow-500/10 text-yellow-400 hover:bg-yellow-500/20'
|
||||
? 'bg-yellow-50 dark:bg-yellow-500/10 text-yellow-700 dark:text-yellow-400 hover:bg-yellow-500/20'
|
||||
: 'bg-bambu-dark text-bambu-gray/50 hover:bg-bambu-dark-tertiary hover:text-white'
|
||||
}`}
|
||||
title={!hasPermission('printers:control') ? t('printers.permission.noControl') : (status.chamber_light ? t('printers.chamberLightOff') : t('printers.chamberLightOn'))}
|
||||
@@ -4049,8 +4049,8 @@ function PrinterCard({
|
||||
{(['P2S', 'X2D', 'H2D', 'H2C', 'H2S'].includes(printer.model ?? '')) && (() => {
|
||||
const isHeating = status.airduct_mode === 1;
|
||||
const Icon = isHeating ? Flame : Snowflake;
|
||||
const color = isHeating ? 'text-orange-400' : 'text-sky-400';
|
||||
const bg = isHeating ? 'bg-orange-500/10 text-orange-400 hover:bg-orange-500/20' : 'bg-sky-500/10 text-sky-400 hover:bg-sky-500/20';
|
||||
const color = isHeating ? 'text-orange-600 dark:text-orange-400' : 'text-sky-600 dark:text-sky-400';
|
||||
const bg = isHeating ? 'bg-orange-50 dark:bg-orange-500/10 text-orange-700 dark:text-orange-400 hover:bg-orange-500/20' : 'bg-sky-50 dark:bg-sky-500/10 text-sky-700 dark:text-sky-400 hover:bg-sky-500/20';
|
||||
return (
|
||||
<div className="relative">
|
||||
<button
|
||||
@@ -4097,7 +4097,7 @@ function PrinterCard({
|
||||
const canControl = hasPermission('printers:control');
|
||||
const disabled = isPrinting || !canControl;
|
||||
const bambuIsPlateBelow = true; // positive Z moves plate away from nozzle
|
||||
const jogButtonClass = 'flex h-8 w-8 items-center justify-center rounded bg-indigo-500/15 text-indigo-300 transition-colors hover:bg-indigo-500/30 disabled:cursor-not-allowed disabled:opacity-50';
|
||||
const jogButtonClass = 'flex h-8 w-8 items-center justify-center rounded bg-indigo-100 dark:bg-indigo-500/15 text-indigo-700 dark:text-indigo-300 transition-colors hover:bg-indigo-200 dark:hover:bg-indigo-500/30 disabled:cursor-not-allowed disabled:opacity-50';
|
||||
const requestZJog = (direction: 1 | -1) => {
|
||||
const signed = direction * bedJogStep * (bambuIsPlateBelow ? 1 : -1);
|
||||
const warnedKey = `bambuddy.bedJog.warned.${printer.id}`;
|
||||
@@ -4125,7 +4125,7 @@ function PrinterCard({
|
||||
className={`${iconControlClass} ${
|
||||
disabled
|
||||
? 'bg-bambu-dark text-bambu-gray/50 cursor-not-allowed'
|
||||
: 'bg-indigo-500/10 text-indigo-400 hover:bg-indigo-500/20'
|
||||
: 'bg-indigo-50 dark:bg-indigo-500/10 text-indigo-700 dark:text-indigo-400 hover:bg-indigo-500/20'
|
||||
}`}
|
||||
title={!canControl ? t('printers.permission.noControl') : isPrinting ? t('printers.bedJog.disabledWhilePrinting') : t('printers.bedJog.title')}
|
||||
>
|
||||
@@ -4268,7 +4268,7 @@ function PrinterCard({
|
||||
disabled={!status.connected || plateDetectionMutation.isPending || !hasPermission('printers:update')}
|
||||
className={`${iconControlClass} rounded-r-none ${
|
||||
printer.plate_detection_enabled
|
||||
? 'bg-green-500/10 text-green-400 hover:bg-green-500/20'
|
||||
? 'bg-green-50 dark:bg-green-500/10 text-green-700 dark:text-green-400 hover:bg-green-500/20'
|
||||
: 'bg-bambu-dark text-bambu-gray/50 hover:bg-bambu-dark-tertiary hover:text-white'
|
||||
}`}
|
||||
title={!hasPermission('printers:update') ? t('printers.plateDetection.noPermission') : (printer.plate_detection_enabled ? t('printers.plateDetection.enabledClick') : t('printers.plateDetection.disabledClick'))}
|
||||
@@ -4284,7 +4284,7 @@ function PrinterCard({
|
||||
disabled={!status.connected || isCheckingPlate || !hasPermission('printers:update')}
|
||||
className={`flex h-8 w-8 items-center justify-center rounded-r-lg border-l border-bambu-dark-tertiary transition-colors disabled:opacity-50 disabled:cursor-not-allowed ${
|
||||
printer.plate_detection_enabled
|
||||
? 'bg-green-500/10 text-green-400 hover:bg-green-500/20'
|
||||
? 'bg-green-50 dark:bg-green-500/10 text-green-700 dark:text-green-400 hover:bg-green-500/20'
|
||||
: 'bg-bambu-dark text-bambu-gray/50 hover:bg-bambu-dark-tertiary hover:text-white'
|
||||
}`}
|
||||
title={!hasPermission('printers:update') ? t('printers.plateDetection.noPermission') : t('printers.plateDetection.manageCalibration')}
|
||||
@@ -4306,7 +4306,7 @@ function PrinterCard({
|
||||
disabled={!isPrinting || !hasPermission('printers:control')}
|
||||
className={`${iconControlClass} ${
|
||||
isPrinting
|
||||
? 'bg-amber-500/10 text-amber-400 hover:bg-amber-500/20'
|
||||
? 'bg-amber-50 dark:bg-amber-500/10 text-amber-700 dark:text-amber-400 hover:bg-amber-500/20'
|
||||
: 'bg-bambu-dark text-bambu-gray/50 cursor-not-allowed'
|
||||
}`}
|
||||
title={isPrinting ? t('printers.speed.title') : undefined}
|
||||
@@ -4361,7 +4361,7 @@ function PrinterCard({
|
||||
? unavailablePrintActionClass
|
||||
: isPaused
|
||||
? 'bg-bambu-green/20 text-bambu-green hover:bg-bambu-green/30'
|
||||
: 'bg-yellow-500/20 text-yellow-400 hover:bg-yellow-500/30'
|
||||
: 'bg-yellow-100 dark:bg-yellow-500/20 text-yellow-700 dark:text-yellow-400 hover:bg-yellow-500/30'
|
||||
}
|
||||
`}
|
||||
title={!hasPermission('printers:control') ? t('printers.permission.noControl') : (isPaused ? t('printers.resume') : t('printers.pause'))}
|
||||
@@ -4383,7 +4383,7 @@ function PrinterCard({
|
||||
${printControlClass}
|
||||
${stopUnavailable
|
||||
? unavailablePrintActionClass
|
||||
: 'bg-red-500/20 text-red-400 hover:bg-red-500/30'
|
||||
: 'bg-red-100 dark:bg-red-500/20 text-red-700 dark:text-red-400 hover:bg-red-500/30'
|
||||
}
|
||||
`}
|
||||
title={!hasPermission('printers:control') ? t('printers.permission.noControl') : t('printers.stop')}
|
||||
@@ -4527,7 +4527,7 @@ function PrinterCard({
|
||||
}}
|
||||
className={`ml-1 flex items-center gap-0.5 px-1 py-0.5 rounded text-[9px] transition-colors ${
|
||||
ams.dry_time > 0
|
||||
? 'bg-amber-500/20 text-amber-400'
|
||||
? 'bg-amber-100 dark:bg-amber-500/20 text-amber-700 dark:text-amber-400'
|
||||
: ams.dry_sf_reason?.length
|
||||
? 'bg-bambu-dark text-bambu-gray/50 cursor-not-allowed'
|
||||
: 'bg-bambu-dark text-bambu-gray hover:text-white hover:bg-bambu-dark/80'
|
||||
@@ -4542,15 +4542,15 @@ function PrinterCard({
|
||||
</div>
|
||||
{/* Drying status bar */}
|
||||
{ams.dry_time > 0 && (
|
||||
<div className="flex items-center gap-2 rounded-lg bg-amber-500/10 px-2 py-1 text-[9px]">
|
||||
<Flame className="w-3 h-3 text-amber-400 shrink-0" />
|
||||
<span className="text-amber-400 font-medium">{t('printers.drying.active')}</span>
|
||||
<div className="flex items-center gap-2 rounded-lg bg-amber-50 dark:bg-amber-500/10 px-2 py-1 text-[9px]">
|
||||
<Flame className="w-3 h-3 text-amber-600 dark:text-amber-400 shrink-0" />
|
||||
<span className="text-amber-700 dark:text-amber-400 font-medium">{t('printers.drying.active')}</span>
|
||||
{ams.dry_filament && ams.dry_target_temp != null && (
|
||||
<span className="text-amber-300/70">
|
||||
<span className="text-amber-700/80 dark:text-amber-300/70">
|
||||
{t('printers.drying.targetSummary', { filament: ams.dry_filament, temp: ams.dry_target_temp })}
|
||||
</span>
|
||||
)}
|
||||
<span className="text-amber-300/70">
|
||||
<span className="text-amber-700/80 dark:text-amber-300/70">
|
||||
{t('printers.drying.timeRemaining', {
|
||||
time: ams.dry_time >= 60
|
||||
? `${Math.floor(ams.dry_time / 60)}h ${ams.dry_time % 60}m`
|
||||
@@ -4560,7 +4560,7 @@ function PrinterCard({
|
||||
<button
|
||||
onClick={() => stopDryingMutation.mutate(ams.id)}
|
||||
disabled={stopDryingMutation.isPending}
|
||||
className="ml-auto text-amber-400 hover:text-amber-300 transition-colors disabled:opacity-50"
|
||||
className="ml-auto text-amber-700 dark:text-amber-400 hover:text-amber-900 dark:hover:text-amber-300 transition-colors disabled:opacity-50"
|
||||
title={t('printers.drying.stop')}
|
||||
>
|
||||
<X className="w-3 h-3" />
|
||||
@@ -5013,7 +5013,7 @@ function PrinterCard({
|
||||
}}
|
||||
className={`flex items-center gap-0.5 px-1 py-0.5 rounded text-[9px] transition-colors ${
|
||||
ams.dry_time > 0
|
||||
? 'bg-amber-500/20 text-amber-400'
|
||||
? 'bg-amber-100 dark:bg-amber-500/20 text-amber-700 dark:text-amber-400'
|
||||
: 'bg-bambu-dark text-bambu-gray hover:text-white hover:bg-bambu-dark/80'
|
||||
}`}
|
||||
title={ams.dry_time > 0 ? t('printers.drying.stop') : t('printers.drying.start')}
|
||||
@@ -5025,14 +5025,14 @@ function PrinterCard({
|
||||
</div>
|
||||
{/* HT AMS drying status bar */}
|
||||
{ams.dry_time > 0 && (
|
||||
<div className="flex items-center gap-1.5 overflow-hidden whitespace-nowrap rounded-lg bg-amber-500/10 px-2 py-1 text-[9px]">
|
||||
<Flame className="w-3 h-3 text-amber-400 shrink-0" />
|
||||
<div className="flex items-center gap-1.5 overflow-hidden whitespace-nowrap rounded-lg bg-amber-50 dark:bg-amber-500/10 px-2 py-1 text-[9px]">
|
||||
<Flame className="w-3 h-3 text-amber-600 dark:text-amber-400 shrink-0" />
|
||||
{ams.dry_filament && ams.dry_target_temp != null && (
|
||||
<span className="text-amber-300/70 text-[8px] truncate">
|
||||
<span className="text-amber-700/80 dark:text-amber-300/70 text-[8px] truncate">
|
||||
{t('printers.drying.targetSummary', { filament: ams.dry_filament, temp: ams.dry_target_temp })}
|
||||
</span>
|
||||
)}
|
||||
<span className="text-amber-300/70 text-[8px] truncate">
|
||||
<span className="text-amber-700/80 dark:text-amber-300/70 text-[8px] truncate">
|
||||
{ams.dry_time >= 60
|
||||
? `${Math.floor(ams.dry_time / 60)}h ${ams.dry_time % 60}m`
|
||||
: `${ams.dry_time}m`}
|
||||
@@ -5040,7 +5040,7 @@ function PrinterCard({
|
||||
<button
|
||||
onClick={() => stopDryingMutation.mutate(ams.id)}
|
||||
disabled={stopDryingMutation.isPending}
|
||||
className="ml-auto text-amber-400 hover:text-amber-300 transition-colors disabled:opacity-50 shrink-0"
|
||||
className="ml-auto text-amber-700 dark:text-amber-400 hover:text-amber-900 dark:hover:text-amber-300 transition-colors disabled:opacity-50 shrink-0"
|
||||
title={t('printers.drying.stop')}
|
||||
>
|
||||
<X className="w-3 h-3" />
|
||||
@@ -5551,7 +5551,7 @@ function PrinterCard({
|
||||
{/* HA entity buttons row */}
|
||||
{scriptPlugs && scriptPlugs.length > 0 && (
|
||||
<div className="flex items-center gap-2 mt-2">
|
||||
<Home className="w-3.5 h-3.5 text-blue-400 flex-shrink-0" />
|
||||
<Home className="w-3.5 h-3.5 text-blue-600 dark:text-blue-400 flex-shrink-0" />
|
||||
<span className="text-xs text-bambu-gray">HA:</span>
|
||||
<div className="h-[2px] w-5 bg-bambu-dark-tertiary/50" />
|
||||
<div className="flex flex-wrap gap-1">
|
||||
@@ -5569,7 +5569,7 @@ function PrinterCard({
|
||||
}}
|
||||
disabled={runScriptMutation.isPending}
|
||||
title={`${isScript ? 'Run' : 'Toggle'} ${script.ha_entity_id}`}
|
||||
className="px-2 py-0.5 text-xs bg-blue-500/20 text-blue-400 hover:bg-blue-500/30 rounded transition-colors flex items-center gap-1"
|
||||
className="px-2 py-0.5 text-xs bg-blue-100 dark:bg-blue-500/20 text-blue-700 dark:text-blue-400 hover:bg-blue-500/30 rounded transition-colors flex items-center gap-1"
|
||||
>
|
||||
<Play className="w-2.5 h-2.5" />
|
||||
{script.name}
|
||||
@@ -5759,8 +5759,8 @@ function PrinterCard({
|
||||
<div className="p-4 space-y-4">
|
||||
{plateCheckResult.needs_calibration ? (
|
||||
<>
|
||||
<div className="p-3 rounded-lg bg-blue-500/20 border border-blue-500/50">
|
||||
<p className="font-medium text-blue-400">
|
||||
<div className="p-3 rounded-lg bg-blue-100 dark:bg-blue-500/20 border border-blue-300 dark:border-blue-500/50">
|
||||
<p className="font-medium text-blue-700 dark:text-blue-400">
|
||||
{t('printers.plateDetection.calibrationRequired')}
|
||||
</p>
|
||||
<p className="text-sm text-bambu-gray mt-1" dangerouslySetInnerHTML={{ __html: t('printers.plateDetection.calibrationInstructions') }} />
|
||||
@@ -5772,8 +5772,8 @@ function PrinterCard({
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className={`p-3 rounded-lg ${plateCheckResult.is_empty ? 'bg-green-500/20 border border-green-500/50' : 'bg-yellow-500/20 border border-yellow-500/50'}`}>
|
||||
<p className={`font-medium ${plateCheckResult.is_empty ? 'text-green-400' : 'text-yellow-400'}`}>
|
||||
<div className={`p-3 rounded-lg ${plateCheckResult.is_empty ? 'bg-green-100 dark:bg-green-500/20 border border-green-300 dark:border-green-500/50' : 'bg-yellow-100 dark:bg-yellow-500/20 border border-yellow-300 dark:border-yellow-500/50'}`}>
|
||||
<p className={`font-medium ${plateCheckResult.is_empty ? 'text-green-700 dark:text-green-400' : 'text-yellow-700 dark:text-yellow-400'}`}>
|
||||
{plateCheckResult.is_empty ? t('printers.plateDetection.plateEmpty') : t('printers.plateDetection.objectsDetected')}
|
||||
</p>
|
||||
<p className="text-sm text-bambu-gray mt-1">
|
||||
@@ -6120,7 +6120,7 @@ function PrinterCard({
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/70 p-4">
|
||||
<div className="bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-lg shadow-xl w-full max-w-sm p-5">
|
||||
<div className="flex items-start gap-3 mb-4">
|
||||
<AlertTriangle className="w-5 h-5 text-yellow-400 flex-shrink-0 mt-0.5" />
|
||||
<AlertTriangle className="w-5 h-5 text-yellow-600 dark:text-yellow-400 flex-shrink-0 mt-0.5" />
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-white mb-1">
|
||||
{t('printers.bedJog.notHomedTitle')}
|
||||
@@ -6147,7 +6147,7 @@ function PrinterCard({
|
||||
bedJogMutation.mutate({ distance: d, force: true });
|
||||
setShowNotHomedModal(null);
|
||||
}}
|
||||
className="w-full px-3 py-2 rounded-lg text-xs font-medium bg-yellow-500/20 text-yellow-400 hover:bg-yellow-500/30 transition-colors"
|
||||
className="w-full px-3 py-2 rounded-lg text-xs font-medium bg-yellow-100 dark:bg-yellow-500/20 text-yellow-700 dark:text-yellow-400 hover:bg-yellow-500/30 transition-colors"
|
||||
>
|
||||
{t('printers.bedJog.moveAnyway')}
|
||||
</button>
|
||||
@@ -6764,7 +6764,7 @@ export function AddPrinterModal({
|
||||
</Button>
|
||||
|
||||
{discoveryError && (
|
||||
<div className="mt-2 text-sm text-red-400">{discoveryError}</div>
|
||||
<div className="mt-2 text-sm text-red-700 dark:text-red-400">{discoveryError}</div>
|
||||
)}
|
||||
|
||||
{newPrinters.length > 0 && (
|
||||
@@ -6925,10 +6925,10 @@ export function AddPrinterModal({
|
||||
{t('diagnostic.runButton')}
|
||||
</button>
|
||||
{saveWarning ? (
|
||||
<div className="rounded-lg bg-amber-500/10 border border-amber-500/30 p-3 space-y-3">
|
||||
<div className="rounded-lg bg-amber-50 dark:bg-amber-500/10 border border-amber-300 dark:border-amber-500/30 p-3 space-y-3">
|
||||
<div className="flex items-start gap-2">
|
||||
<AlertTriangle className="w-4 h-4 mt-0.5 flex-shrink-0 text-amber-400" />
|
||||
<p className="text-sm text-amber-300">{t('printers.addPreflight.warning')}</p>
|
||||
<AlertTriangle className="w-4 h-4 mt-0.5 flex-shrink-0 text-amber-600 dark:text-amber-400" />
|
||||
<p className="text-sm text-amber-700 dark:text-amber-300">{t('printers.addPreflight.warning')}</p>
|
||||
</div>
|
||||
<DiagnosticChecklist result={saveWarning} />
|
||||
<div className="flex gap-3">
|
||||
@@ -7051,9 +7051,9 @@ function FirmwareUpdateModal({
|
||||
<Card className="w-full max-w-md mx-4">
|
||||
<CardContent>
|
||||
<div className="flex items-start gap-3 mb-4">
|
||||
<div className={`p-2 rounded-full ${firmwareInfo.update_available ? 'bg-orange-500/20' : 'bg-status-ok/20'}`}>
|
||||
<div className={`p-2 rounded-full ${firmwareInfo.update_available ? 'bg-orange-100 dark:bg-orange-500/20' : 'bg-status-ok/20'}`}>
|
||||
{firmwareInfo.update_available
|
||||
? <Download className="w-5 h-5 text-orange-400" />
|
||||
? <Download className="w-5 h-5 text-orange-600 dark:text-orange-400" />
|
||||
: <CheckCircle className="w-5 h-5 text-status-ok" />}
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
@@ -7085,12 +7085,12 @@ function FirmwareUpdateModal({
|
||||
{showSecondLine && (
|
||||
<div className="flex justify-between items-center text-sm mt-1">
|
||||
<span className="text-bambu-gray">{t('printers.firmwareModal.latestVersion')}</span>
|
||||
<span className="text-orange-400 font-mono">{displayVersion}</span>
|
||||
<span className="text-orange-700 dark:text-orange-400 font-mono">{displayVersion}</span>
|
||||
</div>
|
||||
)}
|
||||
{displayNotes && (
|
||||
<details className="mt-3 text-sm" open={!showSecondLine} key={displayVersion ?? 'none'}>
|
||||
<summary className={`cursor-pointer hover:underline ${showSecondLine ? 'text-orange-400' : 'text-status-ok'}`}>
|
||||
<summary className={`cursor-pointer hover:underline ${showSecondLine ? 'text-orange-700 dark:text-orange-400' : 'text-status-ok'}`}>
|
||||
{t('printers.firmwareModal.releaseNotes')}
|
||||
</summary>
|
||||
<div className="mt-2 text-bambu-gray text-xs max-h-40 overflow-y-auto whitespace-pre-wrap">
|
||||
@@ -7121,8 +7121,8 @@ function FirmwareUpdateModal({
|
||||
const relClass = isCurrent
|
||||
? 'text-bambu-gray'
|
||||
: cmp > 0
|
||||
? 'text-orange-400'
|
||||
: 'text-blue-400';
|
||||
? 'text-orange-700 dark:text-orange-400'
|
||||
: 'text-blue-700 dark:text-blue-400';
|
||||
return (
|
||||
<button
|
||||
key={v.version}
|
||||
@@ -7130,7 +7130,7 @@ function FirmwareUpdateModal({
|
||||
disabled={!v.file_available || !canUpdate || isCurrent}
|
||||
onClick={() => setSelectedVersion(v.version)}
|
||||
className={`w-full text-left px-3 py-2 text-sm flex items-center justify-between gap-2 transition-colors ${
|
||||
isSelected ? 'bg-orange-500/10' : 'hover:bg-bambu-dark'
|
||||
isSelected ? 'bg-orange-50 dark:bg-orange-500/10' : 'hover:bg-bambu-dark'
|
||||
} ${!v.file_available || !canUpdate || isCurrent ? 'opacity-60 cursor-not-allowed' : 'cursor-pointer'}`}
|
||||
>
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
@@ -7139,7 +7139,7 @@ function FirmwareUpdateModal({
|
||||
</div>
|
||||
<span className={`text-xs px-2 py-0.5 rounded-full ${
|
||||
isCurrent
|
||||
? 'bg-blue-500/15 text-blue-400 border border-blue-500/30'
|
||||
? 'bg-blue-50 dark:bg-blue-500/15 text-blue-700 dark:text-blue-400 border border-blue-300 dark:border-blue-500/30'
|
||||
: v.file_available
|
||||
? 'bg-bambu-green/15 text-bambu-green border border-bambu-green/30'
|
||||
: 'bg-bambu-gray/10 text-bambu-gray border border-bambu-gray/30'
|
||||
@@ -7173,7 +7173,7 @@ function FirmwareUpdateModal({
|
||||
) : (
|
||||
<div className="space-y-1">
|
||||
{prepareInfo.errors.map((error, i) => (
|
||||
<div key={i} className="flex items-center gap-2 text-red-400 text-sm">
|
||||
<div key={i} className="flex items-center gap-2 text-red-700 dark:text-red-400 text-sm">
|
||||
<AlertCircle className="w-4 h-4 flex-shrink-0" />
|
||||
{error}
|
||||
</div>
|
||||
@@ -7201,7 +7201,7 @@ function FirmwareUpdateModal({
|
||||
</div>
|
||||
<p className="text-xs text-bambu-gray mt-1">{uploadStatus.message}</p>
|
||||
{uploadStatus.error && (
|
||||
<p className="text-xs text-red-400 mt-1">{uploadStatus.error}</p>
|
||||
<p className="text-xs text-red-700 dark:text-red-400 mt-1">{uploadStatus.error}</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
@@ -7459,7 +7459,7 @@ function EditPrinterModal({
|
||||
className="rounded border-bambu-dark-tertiary bg-bambu-dark text-amber-400 focus:ring-amber-400"
|
||||
/>
|
||||
<label htmlFor="edit_maintenance_mode" className="text-sm text-bambu-gray flex items-center gap-1.5">
|
||||
<Wrench className="w-3.5 h-3.5 text-amber-400" />
|
||||
<Wrench className="w-3.5 h-3.5 text-amber-600 dark:text-amber-400" />
|
||||
{t('printers.maintenance.editFieldLabel')}
|
||||
</label>
|
||||
</div>
|
||||
@@ -7468,10 +7468,10 @@ function EditPrinterModal({
|
||||
</p>
|
||||
</div>
|
||||
{saveWarning ? (
|
||||
<div className="rounded-lg bg-amber-500/10 border border-amber-500/30 p-3 space-y-3">
|
||||
<div className="rounded-lg bg-amber-50 dark:bg-amber-500/10 border border-amber-300 dark:border-amber-500/30 p-3 space-y-3">
|
||||
<div className="flex items-start gap-2">
|
||||
<AlertTriangle className="w-4 h-4 mt-0.5 flex-shrink-0 text-amber-400" />
|
||||
<p className="text-sm text-amber-300">{t('printers.addPreflight.warning')}</p>
|
||||
<AlertTriangle className="w-4 h-4 mt-0.5 flex-shrink-0 text-amber-600 dark:text-amber-400" />
|
||||
<p className="text-sm text-amber-700 dark:text-amber-300">{t('printers.addPreflight.warning')}</p>
|
||||
</div>
|
||||
<DiagnosticChecklist result={saveWarning} />
|
||||
<div className="flex gap-3">
|
||||
@@ -7557,13 +7557,13 @@ function PowerDropdownItem({
|
||||
return (
|
||||
<div className="flex items-center justify-between px-3 py-2 hover:bg-gray-100 dark:hover:bg-bambu-dark-tertiary">
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
<span className="text-sm text-gray-900 dark:text-white truncate">{printer.name}</span>
|
||||
<span className="text-sm text-white truncate">{printer.name}</span>
|
||||
{plugStatus && (
|
||||
<span
|
||||
className={`text-xs px-1.5 py-0.5 rounded ${
|
||||
plugStatus.state === 'ON'
|
||||
? 'bg-bambu-green/20 text-bambu-green'
|
||||
: 'bg-red-500/20 text-red-400'
|
||||
: 'bg-red-100 dark:bg-red-500/20 text-red-700 dark:text-red-400'
|
||||
}`}
|
||||
>
|
||||
{plugStatus.state || '?'}
|
||||
|
||||
@@ -600,7 +600,7 @@ function PresetDetailModal({
|
||||
{/* Footer */}
|
||||
{showDeleteConfirm ? (
|
||||
<div className="flex-shrink-0 p-4 border-t border-bambu-dark-tertiary bg-red-500/5">
|
||||
<div className="flex items-center gap-2 mb-3 text-red-400">
|
||||
<div className="flex items-center gap-2 mb-3 text-red-700 dark:text-red-400">
|
||||
<AlertTriangle className="w-5 h-5" />
|
||||
<span className="font-medium">{t('profiles.presets.deleteConfirm')}</span>
|
||||
</div>
|
||||
@@ -770,9 +770,9 @@ function TemplatesModal({
|
||||
};
|
||||
|
||||
const typeLabels = {
|
||||
filament: { label: t('profiles.presets.types.filament'), icon: Droplet, color: 'text-amber-400' },
|
||||
print: { label: t('profiles.presets.types.process'), icon: Settings2, color: 'text-blue-400' },
|
||||
printer: { label: t('profiles.presets.types.printer'), icon: PrinterIcon, color: 'text-purple-400' },
|
||||
filament: { label: t('profiles.presets.types.filament'), icon: Droplet, color: 'text-amber-600 dark:text-amber-400' },
|
||||
print: { label: t('profiles.presets.types.process'), icon: Settings2, color: 'text-blue-600 dark:text-blue-400' },
|
||||
printer: { label: t('profiles.presets.types.printer'), icon: PrinterIcon, color: 'text-purple-600 dark:text-purple-400' },
|
||||
};
|
||||
|
||||
const templateToDelete = deleteConfirmId ? templates.find(tpl => tpl.id === deleteConfirmId) : null;
|
||||
@@ -803,7 +803,7 @@ function TemplatesModal({
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div className="p-2 bg-red-500/20 rounded-lg">
|
||||
<AlertTriangle className="w-6 h-6 text-red-400" />
|
||||
<AlertTriangle className="w-6 h-6 text-red-600 dark:text-red-400" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-white">{t('profiles.templates.deleteTitle')}</h3>
|
||||
@@ -832,7 +832,7 @@ function TemplatesModal({
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between p-4 border-b border-bambu-dark-tertiary">
|
||||
<h2 className="text-lg font-semibold text-white flex items-center gap-2">
|
||||
<Sparkles className="w-5 h-5 text-amber-400" />
|
||||
<Sparkles className="w-5 h-5 text-amber-600 dark:text-amber-400" />
|
||||
{t('profiles.templates.title')}
|
||||
</h2>
|
||||
<button onClick={onClose} className="text-bambu-gray hover:text-white">
|
||||
@@ -909,7 +909,7 @@ function TemplatesModal({
|
||||
}`}
|
||||
/>
|
||||
{editSettingsError && (
|
||||
<p className="text-xs text-red-400 mt-1">{editSettingsError}</p>
|
||||
<p className="text-xs text-red-700 dark:text-red-400 mt-1">{editSettingsError}</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
@@ -964,7 +964,7 @@ function TemplatesModal({
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setDeleteConfirmId(template.id)}
|
||||
className="p-1 text-bambu-gray hover:text-red-400"
|
||||
className="p-1 text-bambu-gray hover:text-red-600 dark:hover:text-red-400"
|
||||
title={t('common.delete')}
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
@@ -1117,7 +1117,7 @@ function DiffModal({
|
||||
{/* Header */}
|
||||
<div className="flex-shrink-0 flex items-center justify-between p-4 border-b border-bambu-dark-tertiary">
|
||||
<h2 className="text-lg font-semibold text-white flex items-center gap-2">
|
||||
<GitCompare className="w-5 h-5 text-blue-400" />
|
||||
<GitCompare className="w-5 h-5 text-blue-600 dark:text-blue-400" />
|
||||
{t('profiles.diff.title')}
|
||||
</h2>
|
||||
<button onClick={onClose} className="text-bambu-gray hover:text-white">
|
||||
@@ -1140,15 +1140,15 @@ function DiffModal({
|
||||
{/* Stats and filters */}
|
||||
<div className="flex-shrink-0 flex items-center justify-between p-4 border-b border-bambu-dark-tertiary">
|
||||
<div className="flex items-center gap-4 text-sm">
|
||||
<span className="flex items-center gap-1 text-green-400">
|
||||
<span className="flex items-center gap-1 text-green-700 dark:text-green-400">
|
||||
<PlusIcon className="w-3.5 h-3.5" />
|
||||
{stats.added} {t('profiles.diff.added')}
|
||||
</span>
|
||||
<span className="flex items-center gap-1 text-red-400">
|
||||
<span className="flex items-center gap-1 text-red-700 dark:text-red-400">
|
||||
<MinusIcon className="w-3.5 h-3.5" />
|
||||
{stats.removed} {t('profiles.diff.removed')}
|
||||
</span>
|
||||
<span className="flex items-center gap-1 text-amber-400">
|
||||
<span className="flex items-center gap-1 text-amber-700 dark:text-amber-400">
|
||||
<ArrowRight className="w-3.5 h-3.5" />
|
||||
{stats.changed} {t('profiles.diff.changed')}
|
||||
</span>
|
||||
@@ -1221,9 +1221,9 @@ function DiffModal({
|
||||
}[entry.status];
|
||||
|
||||
const statusIcon = {
|
||||
added: <PlusIcon className="w-3.5 h-3.5 text-green-400" />,
|
||||
removed: <MinusIcon className="w-3.5 h-3.5 text-red-400" />,
|
||||
changed: <ArrowRight className="w-3.5 h-3.5 text-amber-400" />,
|
||||
added: <PlusIcon className="w-3.5 h-3.5 text-green-600 dark:text-green-400" />,
|
||||
removed: <MinusIcon className="w-3.5 h-3.5 text-red-600 dark:text-red-400" />,
|
||||
changed: <ArrowRight className="w-3.5 h-3.5 text-amber-600 dark:text-amber-400" />,
|
||||
same: <Equal className="w-3.5 h-3.5 text-bambu-gray-dark" />,
|
||||
}[entry.status];
|
||||
|
||||
@@ -1237,7 +1237,7 @@ function DiffModal({
|
||||
</td>
|
||||
<td className="p-3">
|
||||
<span className={`text-sm font-mono break-all ${
|
||||
entry.status === 'removed' ? 'text-red-300' :
|
||||
entry.status === 'removed' ? 'text-red-700 dark:text-red-300' :
|
||||
entry.status === 'changed' ? 'text-white' : 'text-bambu-gray'
|
||||
}`}>
|
||||
{formatValue(entry.left)}
|
||||
@@ -1245,7 +1245,7 @@ function DiffModal({
|
||||
</td>
|
||||
<td className="p-3">
|
||||
<span className={`text-sm font-mono break-all ${
|
||||
entry.status === 'added' ? 'text-green-300' :
|
||||
entry.status === 'added' ? 'text-green-700 dark:text-green-300' :
|
||||
entry.status === 'changed' ? 'text-white' : 'text-bambu-gray'
|
||||
}`}>
|
||||
{formatValue(entry.right)}
|
||||
@@ -1839,7 +1839,7 @@ function CreatePresetModal({
|
||||
>
|
||||
<Code className="w-4 h-4" />
|
||||
JSON
|
||||
{jsonError && <AlertCircle className="w-3 h-3 text-red-400" />}
|
||||
{jsonError && <AlertCircle className="w-3 h-3 text-red-600 dark:text-red-400" />}
|
||||
</button>
|
||||
<div className="flex-1 max-[640px]:hidden" />
|
||||
<button
|
||||
@@ -1908,7 +1908,7 @@ function CreatePresetModal({
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h3 className="text-sm font-medium text-white flex items-center gap-2">
|
||||
<Sparkles className="w-4 h-4 text-amber-400" />
|
||||
<Sparkles className="w-4 h-4 text-amber-600 dark:text-amber-400" />
|
||||
{t('profiles.templates.title')}
|
||||
</h3>
|
||||
{Object.keys(settingsObj).filter(k => k !== 'inherits').length > 0 && (
|
||||
@@ -2155,7 +2155,7 @@ function CreatePresetModal({
|
||||
</div>
|
||||
<button
|
||||
onClick={() => updateField(key, undefined)}
|
||||
className="p-1 text-bambu-gray hover:text-red-400 transition-colors"
|
||||
className="p-1 text-bambu-gray hover:text-red-600 dark:hover:text-red-400 transition-colors"
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</button>
|
||||
@@ -2202,7 +2202,7 @@ function CreatePresetModal({
|
||||
{activeTab === 'json' && (
|
||||
<div className="space-y-2">
|
||||
{jsonError && (
|
||||
<div className="flex items-center gap-2 text-red-400 text-sm">
|
||||
<div className="flex items-center gap-2 text-red-700 dark:text-red-400 text-sm">
|
||||
<AlertCircle className="w-4 h-4" />
|
||||
{jsonError}
|
||||
</div>
|
||||
@@ -2592,7 +2592,7 @@ function CloudProfilesView({
|
||||
<div className="mb-4 p-3 bg-blue-500/10 border border-blue-500/30 rounded-lg">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<GitCompare className="w-5 h-5 text-blue-400" />
|
||||
<GitCompare className="w-5 h-5 text-blue-600 dark:text-blue-400" />
|
||||
<span className="text-white font-medium">{t('profiles.cloudView.compareMode')}</span>
|
||||
<span className="text-bambu-gray">
|
||||
{compareSelection[0]
|
||||
@@ -2669,7 +2669,7 @@ function CloudProfilesView({
|
||||
{/* Filament Column */}
|
||||
<div>
|
||||
<div className="flex items-center gap-2 mb-3 px-1">
|
||||
<Droplet className="w-4 h-4 text-amber-400" />
|
||||
<Droplet className="w-4 h-4 text-amber-600 dark:text-amber-400" />
|
||||
<h3 className="text-sm font-medium text-bambu-gray">{t('profiles.cloudView.columns.filament')}</h3>
|
||||
<span className="text-xs text-bambu-gray-dark">
|
||||
({filteredPresets.filter(p => p.type === 'filament').length})
|
||||
@@ -2700,7 +2700,7 @@ function CloudProfilesView({
|
||||
{/* Process Column */}
|
||||
<div>
|
||||
<div className="flex items-center gap-2 mb-3 px-1">
|
||||
<Settings2 className="w-4 h-4 text-blue-400" />
|
||||
<Settings2 className="w-4 h-4 text-blue-600 dark:text-blue-400" />
|
||||
<h3 className="text-sm font-medium text-bambu-gray">{t('profiles.cloudView.columns.process')}</h3>
|
||||
<span className="text-xs text-bambu-gray-dark">
|
||||
({filteredPresets.filter(p => p.type === 'process').length})
|
||||
@@ -2731,7 +2731,7 @@ function CloudProfilesView({
|
||||
{/* Printer Column */}
|
||||
<div>
|
||||
<div className="flex items-center gap-2 mb-3 px-1">
|
||||
<PrinterIcon className="w-4 h-4 text-purple-400" />
|
||||
<PrinterIcon className="w-4 h-4 text-purple-600 dark:text-purple-400" />
|
||||
<h3 className="text-sm font-medium text-bambu-gray">{t('profiles.cloudView.columns.printer')}</h3>
|
||||
<span className="text-xs text-bambu-gray-dark">
|
||||
({filteredPresets.filter(p => p.type === 'printer').length})
|
||||
|
||||
@@ -66,7 +66,7 @@ type TFunction = (key: string, options?: Record<string, unknown>) => string;
|
||||
function StatusBadge({ status, t }: { status: string; t: TFunction }) {
|
||||
const colors = {
|
||||
active: 'bg-bambu-green/20 text-bambu-green',
|
||||
completed: 'bg-blue-500/20 text-blue-400',
|
||||
completed: 'bg-blue-100 dark:bg-blue-500/20 text-blue-700 dark:text-blue-400',
|
||||
archived: 'bg-bambu-gray/20 text-bambu-gray',
|
||||
};
|
||||
const color = colors[status as keyof typeof colors] || colors.active;
|
||||
@@ -172,9 +172,9 @@ function ArchiveGrid({ archives, t }: { archives: Archive[]; t: TFunction }) {
|
||||
function PriorityBadge({ priority, t }: { priority: string; t: TFunction }) {
|
||||
const config = {
|
||||
low: { color: 'bg-gray-500/20 text-gray-400', label: t('projectDetail.priority.low') },
|
||||
normal: { color: 'bg-blue-500/20 text-blue-400', label: t('projectDetail.priority.normal') },
|
||||
high: { color: 'bg-orange-500/20 text-orange-400', label: t('projectDetail.priority.high') },
|
||||
urgent: { color: 'bg-red-500/20 text-red-400', label: t('projectDetail.priority.urgent') },
|
||||
normal: { color: 'bg-blue-100 dark:bg-blue-500/20 text-blue-700 dark:text-blue-400', label: t('projectDetail.priority.normal') },
|
||||
high: { color: 'bg-orange-100 dark:bg-orange-500/20 text-orange-700 dark:text-orange-400', label: t('projectDetail.priority.high') },
|
||||
urgent: { color: 'bg-red-100 dark:bg-red-500/20 text-red-700 dark:text-red-400', label: t('projectDetail.priority.urgent') },
|
||||
};
|
||||
const { color, label } = config[priority as keyof typeof config] || config.normal;
|
||||
|
||||
@@ -193,9 +193,9 @@ function getDueDateStatus(dateString: string | null, t: TFunction): { color: str
|
||||
const now = new Date();
|
||||
const diffDays = Math.ceil((dueDate.getTime() - now.getTime()) / (1000 * 60 * 60 * 24));
|
||||
|
||||
if (diffDays < 0) return { color: 'text-red-400', label: t('projectDetail.dueDate.overdue') };
|
||||
if (diffDays === 0) return { color: 'text-orange-400', label: t('projectDetail.dueDate.today') };
|
||||
if (diffDays <= 3) return { color: 'text-yellow-400', label: t('projectDetail.dueDate.daysLeft', { count: diffDays }) };
|
||||
if (diffDays < 0) return { color: 'text-red-700 dark:text-red-400', label: t('projectDetail.dueDate.overdue') };
|
||||
if (diffDays === 0) return { color: 'text-orange-700 dark:text-orange-400', label: t('projectDetail.dueDate.today') };
|
||||
if (diffDays <= 3) return { color: 'text-yellow-700 dark:text-yellow-400', label: t('projectDetail.dueDate.daysLeft', { count: diffDays }) };
|
||||
return { color: 'text-bambu-gray', label: t('projectDetail.dueDate.daysLeft', { count: diffDays }) };
|
||||
}
|
||||
|
||||
@@ -627,13 +627,13 @@ export function ProjectDetailPage() {
|
||||
icon={Clock}
|
||||
label={t('projectDetail.stats.printTime')}
|
||||
value={formatDurationFromHours(stats.total_print_time_hours)}
|
||||
color="text-yellow-400"
|
||||
color="text-yellow-600 dark:text-yellow-400"
|
||||
/>
|
||||
<StatCard
|
||||
icon={Printer}
|
||||
label={t('projectDetail.stats.filamentUsed')}
|
||||
value={formatFilament(stats.total_filament_grams)}
|
||||
color="text-purple-400"
|
||||
color="text-purple-600 dark:text-purple-400"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@@ -692,7 +692,7 @@ export function ProjectDetailPage() {
|
||||
<p className="text-sm text-bambu-gray">
|
||||
{t('projectDetail.cost.total')}: <span className="text-white font-semibold">{currency}{project.budget.toFixed(2)}</span>
|
||||
</p>
|
||||
<p className={`text-sm ${remaining >= 0 ? 'text-bambu-green' : 'text-red-400'}`}>
|
||||
<p className={`text-sm ${remaining >= 0 ? 'text-bambu-green' : 'text-red-700 dark:text-red-400'}`}>
|
||||
{t('projectDetail.cost.remaining')}: <span className="font-semibold">{currency}{remaining.toFixed(2)}</span>
|
||||
</p>
|
||||
</div>
|
||||
@@ -727,7 +727,7 @@ export function ProjectDetailPage() {
|
||||
<span className={`text-xs px-2 py-0.5 rounded ${
|
||||
child.status === 'completed' ? 'bg-status-ok/20 text-status-ok' :
|
||||
child.status === 'archived' ? 'bg-bambu-gray/20 text-bambu-gray' :
|
||||
'bg-blue-500/20 text-blue-400'
|
||||
'bg-blue-100 dark:bg-blue-500/20 text-blue-700 dark:text-blue-400'
|
||||
}`}>
|
||||
{child.status}
|
||||
</span>
|
||||
@@ -946,7 +946,7 @@ export function ProjectDetailPage() {
|
||||
</p>
|
||||
<span className={`text-xs px-1.5 py-0.5 rounded font-medium ${
|
||||
file.file_type === '3mf' ? 'bg-bambu-green/20 text-bambu-green'
|
||||
: (file.file_type === 'gcode' || file.file_type === 'gcode.3mf') ? 'bg-blue-500/20 text-blue-400'
|
||||
: (file.file_type === 'gcode' || file.file_type === 'gcode.3mf') ? 'bg-blue-100 dark:bg-blue-500/20 text-blue-700 dark:text-blue-400'
|
||||
: 'bg-bambu-gray/20 text-bambu-gray'
|
||||
}`}>
|
||||
{file.file_type.toUpperCase()}
|
||||
@@ -1221,7 +1221,7 @@ export function ProjectDetailPage() {
|
||||
href={item.sourcing_url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-1 mt-1 text-xs text-blue-400 hover:text-blue-300 transition-colors"
|
||||
className="flex items-center gap-1 mt-1 text-xs text-blue-700 dark:text-blue-400 hover:text-blue-900 dark:hover:text-blue-300 transition-colors"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<ExternalLink className="w-3 h-3 shrink-0" />
|
||||
@@ -1286,7 +1286,7 @@ export function ProjectDetailPage() {
|
||||
<div className={`w-8 h-8 rounded-full flex items-center justify-center shrink-0 ${
|
||||
event.event_type === 'print_completed' ? 'bg-status-ok/20 text-status-ok' :
|
||||
event.event_type === 'print_failed' ? 'bg-status-error/20 text-status-error' :
|
||||
event.event_type === 'print_started' ? 'bg-yellow-500/20 text-yellow-400' :
|
||||
event.event_type === 'print_started' ? 'bg-yellow-100 dark:bg-yellow-500/20 text-yellow-700 dark:text-yellow-400' :
|
||||
'bg-bambu-dark-tertiary text-bambu-gray'
|
||||
}`}>
|
||||
{event.event_type === 'print_completed' && <CheckCircle className="w-4 h-4" />}
|
||||
@@ -1351,7 +1351,7 @@ export function ProjectDetailPage() {
|
||||
</div>
|
||||
<div className="flex items-center gap-4 text-sm">
|
||||
{stats.in_progress_prints > 0 && (
|
||||
<span className="text-yellow-400">
|
||||
<span className="text-yellow-700 dark:text-yellow-400">
|
||||
{t('projectDetail.queue.printing', { count: stats.in_progress_prints })}
|
||||
</span>
|
||||
)}
|
||||
|
||||
@@ -188,7 +188,7 @@ export function ProjectModal({ project, onClose, onSave, isLoading, currencySymb
|
||||
placeholder={t('projects.urlPlaceholder')}
|
||||
maxLength={2048}
|
||||
/>
|
||||
{urlError && <p className="text-xs text-red-400 mt-1">{urlError}</p>}
|
||||
{urlError && <p className="text-xs text-red-600 dark:text-red-400 mt-1">{urlError}</p>}
|
||||
</div>
|
||||
|
||||
{/* #1155: Cover image — only available when editing an existing project,
|
||||
@@ -514,7 +514,7 @@ function ProjectCard({ project, onClick, onEdit, onDelete, hasPermission, t }: P
|
||||
const getStatusConfig = () => {
|
||||
if (isCompleted) return { icon: CheckCircle2, color: 'text-bambu-green', bg: 'bg-bambu-green/10' };
|
||||
if (isArchived) return { icon: Archive, color: 'text-bambu-gray', bg: 'bg-bambu-gray/10' };
|
||||
if (project.queue_count > 0) return { icon: Clock, color: 'text-blue-400', bg: 'bg-blue-400/10' };
|
||||
if (project.queue_count > 0) return { icon: Clock, color: 'text-blue-600 dark:text-blue-400', bg: 'bg-blue-50 dark:bg-blue-400/10' };
|
||||
return { icon: FolderKanban, color: 'text-bambu-gray', bg: 'bg-bambu-gray/10' };
|
||||
};
|
||||
const statusConfig = getStatusConfig();
|
||||
@@ -679,7 +679,7 @@ function ProjectCard({ project, onClick, onEdit, onDelete, hasPermission, t }: P
|
||||
</button>
|
||||
<button
|
||||
className={`w-full px-3 py-2 text-left text-sm flex items-center gap-2 ${
|
||||
hasPermission('projects:delete') ? 'text-red-400 hover:bg-bambu-dark' : 'text-bambu-gray cursor-not-allowed'
|
||||
hasPermission('projects:delete') ? 'text-red-600 dark:text-red-400 hover:bg-bambu-dark' : 'text-bambu-gray cursor-not-allowed'
|
||||
}`}
|
||||
onClick={() => { if (hasPermission('projects:delete')) { onDelete(); setShowActions(false); } }}
|
||||
disabled={!hasPermission('projects:delete')}
|
||||
@@ -746,7 +746,7 @@ function ProjectCard({ project, onClick, onEdit, onDelete, hasPermission, t }: P
|
||||
)}
|
||||
{/* Failed count */}
|
||||
{project.failed_count > 0 && (
|
||||
<div className="text-xs text-red-400">
|
||||
<div className="text-xs text-red-600 dark:text-red-400">
|
||||
{project.failed_count} {t('projects.failed')}
|
||||
</div>
|
||||
)}
|
||||
@@ -760,13 +760,13 @@ function ProjectCard({ project, onClick, onEdit, onDelete, hasPermission, t }: P
|
||||
</div>
|
||||
)}
|
||||
{project.failed_count > 0 && (
|
||||
<div className="flex items-center gap-1.5 text-red-400">
|
||||
<div className="flex items-center gap-1.5 text-red-600 dark:text-red-400">
|
||||
<AlertTriangle className="w-3.5 h-3.5" />
|
||||
<span>{project.failed_count} {t('projects.failed')}</span>
|
||||
</div>
|
||||
)}
|
||||
{project.queue_count > 0 && (
|
||||
<div className="flex items-center gap-1.5 text-blue-400">
|
||||
<div className="flex items-center gap-1.5 text-blue-600 dark:text-blue-400">
|
||||
<Clock className="w-3.5 h-3.5" />
|
||||
<span>{project.queue_count} {t('projects.inQueue')}</span>
|
||||
</div>
|
||||
@@ -820,7 +820,7 @@ function ProjectCard({ project, onClick, onEdit, onDelete, hasPermission, t }: P
|
||||
<div className="flex items-center justify-between pt-3 border-t border-bambu-dark-tertiary">
|
||||
<div className="flex items-center gap-4 text-xs text-bambu-gray">
|
||||
<div className="flex items-center gap-1.5" title={t('projects.printJobs')}>
|
||||
<Layers className="w-3.5 h-3.5 text-blue-400" />
|
||||
<Layers className="w-3.5 h-3.5 text-blue-600 dark:text-blue-400" />
|
||||
<span>{project.archive_count} {t('projects.plates')}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5" title={t('projects.partsPrinted')}>
|
||||
@@ -828,13 +828,13 @@ function ProjectCard({ project, onClick, onEdit, onDelete, hasPermission, t }: P
|
||||
<span>{project.completed_count} {t('projects.parts')}</span>
|
||||
</div>
|
||||
{project.failed_count > 0 && (
|
||||
<div className="flex items-center gap-1.5 text-red-400" title={t('projects.failedParts')}>
|
||||
<div className="flex items-center gap-1.5 text-red-600 dark:text-red-400" title={t('projects.failedParts')}>
|
||||
<AlertTriangle className="w-3.5 h-3.5" />
|
||||
<span>{project.failed_count}</span>
|
||||
</div>
|
||||
)}
|
||||
{project.queue_count > 0 && (
|
||||
<div className="flex items-center gap-1.5 text-yellow-400" title={t('projects.inQueue')}>
|
||||
<div className="flex items-center gap-1.5 text-yellow-600 dark:text-yellow-400" title={t('projects.inQueue')}>
|
||||
<ListTodo className="w-3.5 h-3.5" />
|
||||
<span>{project.queue_count}</span>
|
||||
</div>
|
||||
|
||||
@@ -85,7 +85,7 @@ function StatusBadge({ status, waitingReason, printerState, t }: { status: Print
|
||||
// Special case: pending with waiting_reason shows as "Waiting"
|
||||
if (status === 'pending' && waitingReason) {
|
||||
return (
|
||||
<span className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-medium border text-purple-400 bg-purple-400/10 border-purple-400/20">
|
||||
<span className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-medium border text-purple-700 dark:text-purple-400 bg-purple-50 dark:bg-purple-400/10 border-purple-200 dark:border-purple-400/20">
|
||||
<Clock className="w-3.5 h-3.5" />
|
||||
{t('queue.status.waiting')}
|
||||
</span>
|
||||
@@ -95,7 +95,7 @@ function StatusBadge({ status, waitingReason, printerState, t }: { status: Print
|
||||
// Special case: printing but printer is paused
|
||||
if (status === 'printing' && printerState === 'PAUSE') {
|
||||
return (
|
||||
<span className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-medium border text-yellow-400 bg-yellow-400/10 border-yellow-400/20">
|
||||
<span className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-medium border text-yellow-700 dark:text-yellow-400 bg-yellow-50 dark:bg-yellow-400/10 border-yellow-200 dark:border-yellow-400/20">
|
||||
<Pause className="w-3.5 h-3.5" />
|
||||
{t('queue.status.paused')}
|
||||
</span>
|
||||
@@ -104,10 +104,10 @@ function StatusBadge({ status, waitingReason, printerState, t }: { status: Print
|
||||
|
||||
const config = {
|
||||
pending: { icon: Clock, color: 'text-status-warning bg-status-warning/10 border-status-warning/20', label: t('queue.status.pending') },
|
||||
printing: { icon: Play, color: 'text-blue-400 bg-blue-400/10 border-blue-400/20', label: t('queue.status.printing') },
|
||||
printing: { icon: Play, color: 'text-blue-700 dark:text-blue-400 bg-blue-50 dark:bg-blue-400/10 border-blue-200 dark:border-blue-400/20', label: t('queue.status.printing') },
|
||||
completed: { icon: CheckCircle, color: 'text-status-ok bg-status-ok/10 border-status-ok/20', label: t('queue.status.completed') },
|
||||
failed: { icon: XCircle, color: 'text-status-error bg-status-error/10 border-status-error/20', label: t('queue.status.failed') },
|
||||
skipped: { icon: SkipForward, color: 'text-orange-400 bg-orange-400/10 border-orange-400/20', label: t('queue.status.skipped') },
|
||||
skipped: { icon: SkipForward, color: 'text-orange-700 dark:text-orange-400 bg-orange-50 dark:bg-orange-400/10 border-orange-200 dark:border-orange-400/20', label: t('queue.status.skipped') },
|
||||
cancelled: { icon: X, color: 'text-gray-400 bg-gray-400/10 border-gray-400/20', label: t('queue.status.cancelled') },
|
||||
};
|
||||
|
||||
@@ -284,7 +284,7 @@ function TriStateToggle({
|
||||
<button
|
||||
onClick={() => onChange(false)}
|
||||
disabled={disabled}
|
||||
className={`px-2 py-1 text-xs rounded ${value === false ? 'bg-red-500/20 text-red-400' : 'text-bambu-gray hover:text-white'} disabled:cursor-not-allowed`}
|
||||
className={`px-2 py-1 text-xs rounded ${value === false ? 'bg-red-100 dark:bg-red-500/20 text-red-700 dark:text-red-400' : 'text-bambu-gray hover:text-white'} disabled:cursor-not-allowed`}
|
||||
>
|
||||
{t('common.off')}
|
||||
</button>
|
||||
@@ -504,14 +504,14 @@ function SortableQueueItem({
|
||||
</Link>
|
||||
) : null}
|
||||
{item.batch_name && (
|
||||
<span className="flex-shrink-0 px-1.5 py-0.5 text-[10px] sm:text-xs bg-cyan-500/20 text-cyan-300 rounded border border-cyan-500/30">
|
||||
<span className="flex-shrink-0 px-1.5 py-0.5 text-[10px] sm:text-xs bg-cyan-100 dark:bg-cyan-500/20 text-cyan-700 dark:text-cyan-300 rounded border border-cyan-300 dark:border-cyan-500/30">
|
||||
{item.batch_name}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap items-center gap-x-3 gap-y-1 text-xs sm:text-sm text-bambu-gray">
|
||||
<span className={`flex items-center gap-1 sm:gap-1.5 ${item.printer_id === null && !item.target_model ? 'text-orange-400' : ''} ${item.target_model && !item.printer_id ? 'text-blue-400' : ''}`}>
|
||||
<span className={`flex items-center gap-1 sm:gap-1.5 ${item.printer_id === null && !item.target_model ? 'text-orange-700 dark:text-orange-400' : ''} ${item.target_model && !item.printer_id ? 'text-blue-700 dark:text-blue-400' : ''}`}>
|
||||
<Printer className="w-3 h-3 sm:w-3.5 sm:h-3.5" />
|
||||
<span className="truncate max-w-[120px] sm:max-w-none">
|
||||
{item.target_model && !item.printer_id
|
||||
@@ -567,24 +567,24 @@ function SortableQueueItem({
|
||||
{/* Options badges */}
|
||||
<div className="flex flex-wrap items-center gap-1.5 sm:gap-2 mt-1.5 sm:mt-2">
|
||||
{item.manual_start && (
|
||||
<span className="text-[10px] sm:text-xs px-1.5 sm:px-2 py-0.5 bg-purple-500/10 text-purple-400 rounded-full border border-purple-500/20 flex items-center gap-1">
|
||||
<span className="text-[10px] sm:text-xs px-1.5 sm:px-2 py-0.5 bg-purple-50 dark:bg-purple-500/10 text-purple-700 dark:text-purple-400 rounded-full border border-purple-200 dark:border-purple-500/20 flex items-center gap-1">
|
||||
<Hand className="w-2.5 h-2.5 sm:w-3 sm:h-3" />
|
||||
{t('queue.badges.staged')}
|
||||
</span>
|
||||
)}
|
||||
{item.require_previous_success && (
|
||||
<span className="text-[10px] sm:text-xs px-1.5 sm:px-2 py-0.5 bg-orange-500/10 text-orange-400 rounded-full border border-orange-500/20">
|
||||
<span className="text-[10px] sm:text-xs px-1.5 sm:px-2 py-0.5 bg-orange-50 dark:bg-orange-500/10 text-orange-700 dark:text-orange-400 rounded-full border border-orange-200 dark:border-orange-500/20">
|
||||
{t('queue.badges.requiresPrevious')}
|
||||
</span>
|
||||
)}
|
||||
{item.auto_off_after && (
|
||||
<span className="text-[10px] sm:text-xs px-1.5 sm:px-2 py-0.5 bg-blue-500/10 text-blue-400 rounded-full border border-blue-500/20 flex items-center gap-1">
|
||||
<span className="text-[10px] sm:text-xs px-1.5 sm:px-2 py-0.5 bg-blue-50 dark:bg-blue-500/10 text-blue-700 dark:text-blue-400 rounded-full border border-blue-200 dark:border-blue-500/20 flex items-center gap-1">
|
||||
<Power className="w-2.5 h-2.5 sm:w-3 sm:h-3" />
|
||||
{t('queue.badges.autoPowerOff')}
|
||||
</span>
|
||||
)}
|
||||
{item.gcode_injection && (
|
||||
<span className="text-[10px] sm:text-xs px-1.5 sm:px-2 py-0.5 bg-emerald-500/10 text-emerald-400 rounded-full border border-emerald-500/20 flex items-center gap-1">
|
||||
<span className="text-[10px] sm:text-xs px-1.5 sm:px-2 py-0.5 bg-emerald-50 dark:bg-emerald-500/10 text-emerald-700 dark:text-emerald-400 rounded-full border border-emerald-200 dark:border-emerald-500/20 flex items-center gap-1">
|
||||
<Code className="w-2.5 h-2.5 sm:w-3 sm:h-3" />
|
||||
{t('queue.badges.gcodeInjection')}
|
||||
</span>
|
||||
@@ -638,7 +638,7 @@ function SortableQueueItem({
|
||||
|
||||
{/* Waiting reason for model-based assignments */}
|
||||
{item.waiting_reason && item.status === 'pending' && (
|
||||
<p className="text-[10px] sm:text-xs text-purple-400 mt-1.5 sm:mt-2 flex items-start gap-1">
|
||||
<p className="text-[10px] sm:text-xs text-purple-700 dark:text-purple-400 mt-1.5 sm:mt-2 flex items-start gap-1">
|
||||
<AlertCircle className="w-3 h-3 mt-0.5 flex-shrink-0" />
|
||||
<span>{item.waiting_reason}</span>
|
||||
</p>
|
||||
@@ -647,7 +647,7 @@ function SortableQueueItem({
|
||||
{/* Filament-short flag from the dispatch pre-flight (#1496). */}
|
||||
{item.filament_short && item.status === 'pending' && (
|
||||
<p
|
||||
className="text-[10px] sm:text-xs text-yellow-400 mt-1.5 sm:mt-2 flex items-start gap-1"
|
||||
className="text-[10px] sm:text-xs text-yellow-700 dark:text-yellow-400 mt-1.5 sm:mt-2 flex items-start gap-1"
|
||||
title={t('queue.filamentShort.rowTooltip')}
|
||||
>
|
||||
<AlertCircle className="w-3 h-3 mt-0.5 flex-shrink-0" />
|
||||
@@ -657,7 +657,7 @@ function SortableQueueItem({
|
||||
|
||||
{/* Error message */}
|
||||
{item.error_message && (
|
||||
<p className="text-[10px] sm:text-xs text-red-400 mt-1.5 sm:mt-2 flex items-center gap-1">
|
||||
<p className="text-[10px] sm:text-xs text-red-700 dark:text-red-400 mt-1.5 sm:mt-2 flex items-center gap-1">
|
||||
<AlertCircle className="w-3 h-3" />
|
||||
{item.error_message}
|
||||
</p>
|
||||
@@ -676,7 +676,7 @@ function SortableQueueItem({
|
||||
onClick={onStop}
|
||||
disabled={!canModify('queue', 'update', item.created_by_id)}
|
||||
title={!canModify('queue', 'update', item.created_by_id) ? t('queue.permissions.noStopPrint') : t('queue.actions.stopPrint')}
|
||||
className="text-red-400 hover:text-red-300 hover:bg-red-500/10 p-1.5 sm:p-2"
|
||||
className="text-red-600 dark:text-red-400 hover:text-red-700 dark:hover:text-red-300 hover:bg-red-50 dark:hover:bg-red-500/10 p-1.5 sm:p-2"
|
||||
>
|
||||
<StopCircle className="w-4 h-4" />
|
||||
</Button>
|
||||
@@ -711,7 +711,7 @@ function SortableQueueItem({
|
||||
onClick={onCancel}
|
||||
disabled={!canModify('queue', 'delete', item.created_by_id)}
|
||||
title={!canModify('queue', 'delete', item.created_by_id) ? t('queue.permissions.noCancel') : t('common.cancel')}
|
||||
className="text-red-400 hover:text-red-300 hover:bg-red-500/10 p-1.5 sm:p-2"
|
||||
className="text-red-600 dark:text-red-400 hover:text-red-700 dark:hover:text-red-300 hover:bg-red-50 dark:hover:bg-red-500/10 p-1.5 sm:p-2"
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</Button>
|
||||
@@ -914,15 +914,15 @@ function SortableBatchRow({
|
||||
</button>
|
||||
<div className="w-10 h-10 sm:w-12 sm:h-12 flex-shrink-0 bg-bambu-dark rounded-lg flex items-center justify-center">
|
||||
{collapsed ? (
|
||||
<Package className="w-5 h-5 text-cyan-300" />
|
||||
<Package className="w-5 h-5 text-cyan-700 dark:text-cyan-300" />
|
||||
) : (
|
||||
<PackageOpen className="w-5 h-5 text-cyan-300" />
|
||||
<PackageOpen className="w-5 h-5 text-cyan-700 dark:text-cyan-300" />
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<p className="text-sm sm:text-base text-white font-medium truncate">{batchRow.batchName}</p>
|
||||
<span className="flex-shrink-0 px-1.5 py-0.5 text-[10px] sm:text-xs bg-cyan-500/15 text-cyan-300 rounded border border-cyan-500/30">
|
||||
<span className="flex-shrink-0 px-1.5 py-0.5 text-[10px] sm:text-xs bg-cyan-100 dark:bg-cyan-500/15 text-cyan-700 dark:text-cyan-300 rounded border border-cyan-300 dark:border-cyan-500/30">
|
||||
{t('queue.batch.label', { count: agg.count })}
|
||||
</span>
|
||||
</div>
|
||||
@@ -940,7 +940,7 @@ function SortableBatchRow({
|
||||
</span>
|
||||
)}
|
||||
{pendingChildren > 0 && rollupStatus === 'pending' && (
|
||||
<span className="flex items-center gap-1 sm:gap-1.5 text-yellow-400">
|
||||
<span className="flex items-center gap-1 sm:gap-1.5 text-yellow-700 dark:text-yellow-400">
|
||||
<Clock className="w-3 h-3 sm:w-3.5 sm:h-3.5" />
|
||||
{t('queue.batch.pendingCount', { count: pendingChildren })}
|
||||
</span>
|
||||
@@ -954,7 +954,7 @@ function SortableBatchRow({
|
||||
size="sm"
|
||||
onClick={onUngroup}
|
||||
title={t('queue.batch.ungroup')}
|
||||
className="text-cyan-300 hover:text-cyan-200 hover:bg-cyan-500/10 p-1.5 sm:p-2"
|
||||
className="text-cyan-700 dark:text-cyan-300 hover:text-cyan-900 dark:hover:text-cyan-200 hover:bg-cyan-50 dark:hover:bg-cyan-500/10 p-1.5 sm:p-2"
|
||||
>
|
||||
<Ungroup className="w-4 h-4" />
|
||||
</Button>
|
||||
@@ -1133,28 +1133,28 @@ function HistorySection({
|
||||
<ChevronDown className="w-4 h-4 text-bambu-gray shrink-0" />
|
||||
)}
|
||||
{collapsed ? (
|
||||
<Package className="w-5 h-5 text-cyan-300 shrink-0" />
|
||||
<Package className="w-5 h-5 text-cyan-700 dark:text-cyan-300 shrink-0" />
|
||||
) : (
|
||||
<PackageOpen className="w-5 h-5 text-cyan-300 shrink-0" />
|
||||
<PackageOpen className="w-5 h-5 text-cyan-700 dark:text-cyan-300 shrink-0" />
|
||||
)}
|
||||
<span className="text-sm text-white font-medium truncate min-w-0 flex-1">
|
||||
{row.batchName}
|
||||
</span>
|
||||
<div className="flex items-center gap-2 text-xs text-bambu-gray shrink-0">
|
||||
{completed > 0 && (
|
||||
<span className="flex items-center gap-1 text-emerald-400">
|
||||
<span className="flex items-center gap-1 text-emerald-700 dark:text-emerald-400">
|
||||
<CheckCircle className="w-3 h-3" />
|
||||
{completed}
|
||||
</span>
|
||||
)}
|
||||
{failed > 0 && (
|
||||
<span className="flex items-center gap-1 text-red-400">
|
||||
<span className="flex items-center gap-1 text-red-700 dark:text-red-400">
|
||||
<XCircle className="w-3 h-3" />
|
||||
{failed}
|
||||
</span>
|
||||
)}
|
||||
{skipped > 0 && (
|
||||
<span className="flex items-center gap-1 text-orange-400">
|
||||
<span className="flex items-center gap-1 text-orange-700 dark:text-orange-400">
|
||||
<SkipForward className="w-3 h-3" />
|
||||
{skipped}
|
||||
</span>
|
||||
@@ -1972,17 +1972,17 @@ export function QueuePage() {
|
||||
{gateBlockedPrinters.map(({ printerId, printerName, skippedCount }) => (
|
||||
<div
|
||||
key={printerId}
|
||||
className="flex items-center gap-3 px-4 py-3 bg-orange-500/10 border border-orange-500/30 rounded-lg"
|
||||
className="flex items-center gap-3 px-4 py-3 bg-orange-50 dark:bg-orange-500/10 border border-orange-300 dark:border-orange-500/30 rounded-lg"
|
||||
>
|
||||
<AlertCircle className="w-5 h-5 text-orange-400 flex-shrink-0" />
|
||||
<AlertCircle className="w-5 h-5 text-orange-600 dark:text-orange-400 flex-shrink-0" />
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="text-sm text-orange-200">
|
||||
<div className="text-sm text-orange-800 dark:text-orange-200">
|
||||
{t('queue.resumeAfterFailure.banner', {
|
||||
printer: printerName,
|
||||
count: skippedCount,
|
||||
})}
|
||||
</div>
|
||||
<div className="text-xs text-orange-200/70 mt-0.5">
|
||||
<div className="text-xs text-orange-800/80 dark:text-orange-200/70 mt-0.5">
|
||||
{t('queue.resumeAfterFailure.bannerHint')}
|
||||
</div>
|
||||
</div>
|
||||
@@ -1990,7 +1990,7 @@ export function QueuePage() {
|
||||
onClick={() =>
|
||||
setResumeConfirm({ printerId, printerName, skippedCount })
|
||||
}
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 bg-orange-500/20 hover:bg-orange-500/30 text-orange-100 text-sm rounded-md border border-orange-500/40 transition-colors flex-shrink-0"
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 bg-orange-100 dark:bg-orange-500/20 hover:bg-orange-200 dark:hover:bg-orange-500/30 text-orange-900 dark:text-orange-100 text-sm rounded-md border border-orange-300 dark:border-orange-500/40 transition-colors flex-shrink-0"
|
||||
>
|
||||
<PlayCircle className="w-4 h-4" />
|
||||
{t('queue.resumeAfterFailure.button')}
|
||||
@@ -2195,7 +2195,7 @@ export function QueuePage() {
|
||||
<div>
|
||||
<div className="flex flex-wrap items-center justify-between gap-2 mb-3 sm:mb-4">
|
||||
<h2 className="text-base sm:text-lg font-semibold text-white flex items-center gap-2">
|
||||
<Clock className="w-4 h-4 sm:w-5 sm:h-5 text-yellow-400" />
|
||||
<Clock className="w-4 h-4 sm:w-5 sm:h-5 text-yellow-600 dark:text-yellow-400" />
|
||||
{t('queue.sections.queued')}
|
||||
<span className="text-xs sm:text-sm font-normal text-bambu-gray">
|
||||
({t('queue.itemCount', { count: pendingItems.length })})
|
||||
@@ -2253,7 +2253,7 @@ export function QueuePage() {
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setGroupBatchModal(true)}
|
||||
className="flex items-center gap-1.5 sm:gap-2 text-xs sm:text-sm text-cyan-300 hover:text-cyan-200"
|
||||
className="flex items-center gap-1.5 sm:gap-2 text-xs sm:text-sm text-cyan-700 dark:text-cyan-300 hover:text-cyan-900 dark:hover:text-cyan-200"
|
||||
title={t('queue.batch.groupAsBatch')}
|
||||
>
|
||||
<Package className="w-3.5 h-3.5 sm:w-4 sm:h-4" />
|
||||
@@ -2275,7 +2275,7 @@ export function QueuePage() {
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => bulkCancelMutation.mutate(selectedItems)}
|
||||
className="flex items-center gap-1.5 sm:gap-2 text-xs sm:text-sm text-red-400 hover:text-red-300"
|
||||
className="flex items-center gap-1.5 sm:gap-2 text-xs sm:text-sm text-red-600 dark:text-red-400 hover:text-red-700 dark:hover:text-red-300"
|
||||
disabled={bulkCancelMutation.isPending || !hasAnyPermission('queue:delete_own', 'queue:delete_all')}
|
||||
title={!hasAnyPermission('queue:delete_own', 'queue:delete_all') ? t('queue.permissions.noCancelItems') : t('queue.bulkEdit.cancelSelected')}
|
||||
>
|
||||
@@ -2325,7 +2325,7 @@ export function QueuePage() {
|
||||
return (
|
||||
<div key={bucket.key}>
|
||||
<div className="flex items-center gap-3 px-3 py-2 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-t-lg">
|
||||
<Printer className={`w-4 h-4 ${bucket.isUnassigned ? 'text-orange-400' : bucket.targetModel ? 'text-blue-400' : 'text-bambu-green'}`} />
|
||||
<Printer className={`w-4 h-4 ${bucket.isUnassigned ? 'text-orange-600 dark:text-orange-400' : bucket.targetModel ? 'text-blue-600 dark:text-blue-400' : 'text-bambu-green'}`} />
|
||||
<span className="font-semibold text-white text-sm">{bucket.label}</span>
|
||||
<span className="text-xs text-bambu-gray flex flex-wrap gap-x-3">
|
||||
<span>{t('queue.itemCount', { count: agg.count })}</span>
|
||||
@@ -2371,7 +2371,7 @@ export function QueuePage() {
|
||||
const name = siblings[0].batch_name || t('queue.batch.defaultName');
|
||||
return (
|
||||
<div className="flex items-center gap-3 px-3 py-2 bg-bambu-dark-secondary border-2 border-cyan-400 rounded-lg shadow-2xl">
|
||||
<Package className="w-4 h-4 text-cyan-300" />
|
||||
<Package className="w-4 h-4 text-cyan-700 dark:text-cyan-300" />
|
||||
<span className="text-sm text-white font-medium">
|
||||
{t('queue.dragGhost.batch', {
|
||||
defaultValue: '{{name}} ({{count}} copies)',
|
||||
@@ -2386,7 +2386,7 @@ export function QueuePage() {
|
||||
if (typeof activeDragId === 'number' && selectedItems.includes(activeDragId) && selectedItems.length > 1) {
|
||||
return (
|
||||
<div className="flex items-center gap-3 px-3 py-2 bg-bambu-dark-secondary border-2 border-cyan-400 rounded-lg shadow-2xl">
|
||||
<Package className="w-4 h-4 text-cyan-300" />
|
||||
<Package className="w-4 h-4 text-cyan-700 dark:text-cyan-300" />
|
||||
<span className="text-sm text-white font-medium">
|
||||
{t('queue.dragGhost.multiCount', { count: selectedItems.length })}
|
||||
</span>
|
||||
@@ -2582,7 +2582,7 @@ function GroupBatchModal({ itemCount, defaultName, isSaving, onSave, onClose, t
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 p-4">
|
||||
<div className="bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-xl p-6 w-full max-w-md">
|
||||
<h3 className="text-lg font-semibold text-white mb-2 flex items-center gap-2">
|
||||
<Package className="w-5 h-5 text-cyan-300" />
|
||||
<Package className="w-5 h-5 text-cyan-700 dark:text-cyan-300" />
|
||||
{t('queue.batch.groupAsBatch')}
|
||||
</h3>
|
||||
<p className="text-sm text-bambu-gray mb-4">
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user