Browse Source

Improve settings menu layout - 1

maziggy 1 month ago
parent
commit
8813de6f80

+ 21 - 5
frontend/src/components/Card.tsx

@@ -1,13 +1,23 @@
+import { createContext, useContext } from 'react';
 import type { ReactNode, MouseEvent, HTMLAttributes } from 'react';
 import type { ReactNode, MouseEvent, HTMLAttributes } from 'react';
 
 
+type CardDensity = 'normal' | 'dense';
+
+const CardDensityContext = createContext<CardDensity>('normal');
+
+export function CardDensityProvider({ density, children }: { density: CardDensity; children: ReactNode }) {
+  return <CardDensityContext.Provider value={density}>{children}</CardDensityContext.Provider>;
+}
+
 interface CardProps extends HTMLAttributes<HTMLDivElement> {
 interface CardProps extends HTMLAttributes<HTMLDivElement> {
   children: ReactNode;
   children: ReactNode;
   className?: string;
   className?: string;
   onClick?: (e: MouseEvent) => void;
   onClick?: (e: MouseEvent) => void;
   onContextMenu?: (e: MouseEvent) => void;
   onContextMenu?: (e: MouseEvent) => void;
+  dense?: boolean;
 }
 }
 
 
-export function Card({ children, className = '', onClick, onContextMenu, ...rest }: CardProps) {
+export function Card({ children, className = '', onClick, onContextMenu, dense: _dense, ...rest }: CardProps) {
   return (
   return (
     <div
     <div
       className={`bg-bambu-dark-secondary rounded-xl border border-bambu-dark-tertiary card-shadow ${className}`}
       className={`bg-bambu-dark-secondary rounded-xl border border-bambu-dark-tertiary card-shadow ${className}`}
@@ -20,14 +30,20 @@ export function Card({ children, className = '', onClick, onContextMenu, ...rest
   );
   );
 }
 }
 
 
-export function CardHeader({ children, className = '' }: CardProps) {
+export function CardHeader({ children, className = '', dense }: CardProps) {
+  const ctxDense = useContext(CardDensityContext) === 'dense';
+  const isDense = dense ?? ctxDense;
+  const padding = isDense ? 'px-4 py-2.5' : 'px-6 py-4';
   return (
   return (
-    <div className={`px-6 py-4 border-b border-bambu-dark-tertiary ${className}`}>
+    <div className={`${padding} border-b border-bambu-dark-tertiary ${className}`}>
       {children}
       {children}
     </div>
     </div>
   );
   );
 }
 }
 
 
-export function CardContent({ children, className = '' }: CardProps) {
-  return <div className={`p-6 ${className}`}>{children}</div>;
+export function CardContent({ children, className = '', dense }: CardProps) {
+  const ctxDense = useContext(CardDensityContext) === 'dense';
+  const isDense = dense ?? ctxDense;
+  const padding = isDense ? 'p-4' : 'p-6';
+  return <div className={`${padding} ${className}`}>{children}</div>;
 }
 }

+ 42 - 0
frontend/src/components/Collapsible.tsx

@@ -0,0 +1,42 @@
+import { useState } from 'react';
+import type { ReactNode } from 'react';
+import { ChevronDown } from 'lucide-react';
+
+interface CollapsibleProps {
+  summary: ReactNode;
+  children: ReactNode;
+  defaultOpen?: boolean;
+  className?: string;
+  summaryClassName?: string;
+}
+
+/**
+ * Lightweight disclosure used for densifying the Settings page.
+ * Renders a clickable summary row and animates open/close via a simple
+ * display swap (no height animation — keeps it snappy and layout-stable).
+ */
+export function Collapsible({
+  summary,
+  children,
+  defaultOpen = false,
+  className = '',
+  summaryClassName = '',
+}: CollapsibleProps) {
+  const [open, setOpen] = useState(defaultOpen);
+  return (
+    <div className={className}>
+      <button
+        type="button"
+        onClick={() => setOpen(o => !o)}
+        className={`w-full flex items-center justify-between gap-2 text-left ${summaryClassName}`}
+        aria-expanded={open}
+      >
+        <div className="flex-1 min-w-0">{summary}</div>
+        <ChevronDown
+          className={`w-4 h-4 text-bambu-gray flex-shrink-0 transition-transform ${open ? 'rotate-180' : ''}`}
+        />
+      </button>
+      {open && <div className="mt-3">{children}</div>}
+    </div>
+  );
+}

+ 22 - 20
frontend/src/components/EmailSettings.tsx

@@ -170,11 +170,11 @@ export function EmailSettings() {
   }
   }
 
 
   const advancedEnabled = advancedAuthStatus?.advanced_auth_enabled ?? false;
   const advancedEnabled = advancedAuthStatus?.advanced_auth_enabled ?? false;
-  const inputClasses = "w-full px-4 py-3 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-lg text-white placeholder-bambu-gray focus:outline-none focus:ring-2 focus:ring-bambu-green/50 focus:border-bambu-green transition-colors";
-  const disabledInputClasses = "w-full px-4 py-3 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-lg text-white/40 placeholder-bambu-gray/40 cursor-not-allowed";
+  const inputClasses = "w-full px-3 py-2 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-lg text-white placeholder-bambu-gray focus:outline-none focus:ring-2 focus:ring-bambu-green/50 focus:border-bambu-green transition-colors";
+  const disabledInputClasses = "w-full px-3 py-2 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-lg text-white/40 placeholder-bambu-gray/40 cursor-not-allowed";
 
 
   return (
   return (
-    <div className="space-y-6">
+    <div className="space-y-3">
       {/* Advanced Authentication Toggle - Always visible */}
       {/* Advanced Authentication Toggle - Always visible */}
       <Card>
       <Card>
         <CardHeader>
         <CardHeader>
@@ -205,7 +205,7 @@ export function EmailSettings() {
           </div>
           </div>
         </CardHeader>
         </CardHeader>
         <CardContent>
         <CardContent>
-          <div className="space-y-4">
+          <div className="space-y-3">
             {advancedEnabled ? (
             {advancedEnabled ? (
               <div className="bg-green-500/10 border border-green-500/30 rounded-lg p-4">
               <div className="bg-green-500/10 border border-green-500/30 rounded-lg p-4">
                 <div className="flex items-start gap-3">
                 <div className="flex items-start gap-3">
@@ -242,8 +242,9 @@ export function EmailSettings() {
         </CardContent>
         </CardContent>
       </Card>
       </Card>
 
 
-      {/* SMTP Configuration */}
-      <div>
+      {/* SMTP Config + Test SMTP side-by-side on lg+ */}
+      <div className="grid grid-cols-1 lg:grid-cols-3 gap-4">
+        <div className="lg:col-span-2">
         <Card>
         <Card>
           <CardHeader>
           <CardHeader>
             <h2 className="text-lg font-semibold text-white">
             <h2 className="text-lg font-semibold text-white">
@@ -251,10 +252,10 @@ export function EmailSettings() {
             </h2>
             </h2>
           </CardHeader>
           </CardHeader>
           <CardContent>
           <CardContent>
-            <div className="space-y-4">
+            <div className="space-y-3">
               {/* Authentication - at the top */}
               {/* Authentication - at the top */}
               <div>
               <div>
-                <label className="block text-sm font-medium text-white mb-2">
+                <label className="block text-sm font-medium text-white mb-1">
                   {t('settings.email.authentication') || 'Authentication'}
                   {t('settings.email.authentication') || 'Authentication'}
                 </label>
                 </label>
                 <select
                 <select
@@ -270,7 +271,7 @@ export function EmailSettings() {
               {/* Username / Password - dimmed when auth disabled */}
               {/* Username / Password - dimmed when auth disabled */}
               <div className={`grid grid-cols-1 md:grid-cols-2 gap-4 transition-opacity ${!smtpSettings.smtp_auth_enabled ? 'opacity-40 pointer-events-none' : ''}`}>
               <div className={`grid grid-cols-1 md:grid-cols-2 gap-4 transition-opacity ${!smtpSettings.smtp_auth_enabled ? 'opacity-40 pointer-events-none' : ''}`}>
                 <div>
                 <div>
-                  <label className="block text-sm font-medium text-white mb-2">
+                  <label className="block text-sm font-medium text-white mb-1">
                     {t('settings.email.username') || 'Username'}
                     {t('settings.email.username') || 'Username'}
                   </label>
                   </label>
                   <input
                   <input
@@ -283,7 +284,7 @@ export function EmailSettings() {
                   />
                   />
                 </div>
                 </div>
                 <div>
                 <div>
-                  <label className="block text-sm font-medium text-white mb-2">
+                  <label className="block text-sm font-medium text-white mb-1">
                     {t('settings.email.password') || 'Password'}
                     {t('settings.email.password') || 'Password'}
                   </label>
                   </label>
                   <input
                   <input
@@ -300,7 +301,7 @@ export function EmailSettings() {
               {/* SMTP Server / Port */}
               {/* SMTP Server / Port */}
               <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
               <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
                 <div>
                 <div>
-                  <label className="block text-sm font-medium text-white mb-2">
+                  <label className="block text-sm font-medium text-white mb-1">
                     {t('settings.email.smtpHost') || 'SMTP Server'} *
                     {t('settings.email.smtpHost') || 'SMTP Server'} *
                   </label>
                   </label>
                   <input
                   <input
@@ -312,7 +313,7 @@ export function EmailSettings() {
                   />
                   />
                 </div>
                 </div>
                 <div>
                 <div>
-                  <label className="block text-sm font-medium text-white mb-2">
+                  <label className="block text-sm font-medium text-white mb-1">
                     {t('settings.email.smtpPort') || 'SMTP Port'}
                     {t('settings.email.smtpPort') || 'SMTP Port'}
                   </label>
                   </label>
                   <input
                   <input
@@ -327,7 +328,7 @@ export function EmailSettings() {
 
 
               {/* Security */}
               {/* Security */}
               <div>
               <div>
-                <label className="block text-sm font-medium text-white mb-2">
+                <label className="block text-sm font-medium text-white mb-1">
                   {t('settings.email.security') || 'Security'}
                   {t('settings.email.security') || 'Security'}
                 </label>
                 </label>
                 <select
                 <select
@@ -344,7 +345,7 @@ export function EmailSettings() {
               {/* From Email / Name */}
               {/* From Email / Name */}
               <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
               <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
                 <div>
                 <div>
-                  <label className="block text-sm font-medium text-white mb-2">
+                  <label className="block text-sm font-medium text-white mb-1">
                     {t('settings.email.fromEmail') || 'From Email'} *
                     {t('settings.email.fromEmail') || 'From Email'} *
                   </label>
                   </label>
                   <input
                   <input
@@ -356,7 +357,7 @@ export function EmailSettings() {
                   />
                   />
                 </div>
                 </div>
                 <div>
                 <div>
-                  <label className="block text-sm font-medium text-white mb-2">
+                  <label className="block text-sm font-medium text-white mb-1">
                     {t('settings.email.fromName') || 'From Name'}
                     {t('settings.email.fromName') || 'From Name'}
                   </label>
                   </label>
                   <input
                   <input
@@ -388,10 +389,10 @@ export function EmailSettings() {
             </div>
             </div>
           </CardContent>
           </CardContent>
         </Card>
         </Card>
-      </div>
+        </div>
 
 
-      {/* Test SMTP */}
-      <div>
+        {/* Test SMTP */}
+        <div>
         <Card>
         <Card>
           <CardHeader>
           <CardHeader>
             <h2 className="text-lg font-semibold text-white">
             <h2 className="text-lg font-semibold text-white">
@@ -399,9 +400,9 @@ export function EmailSettings() {
             </h2>
             </h2>
           </CardHeader>
           </CardHeader>
           <CardContent>
           <CardContent>
-            <div className="space-y-4">
+            <div className="space-y-3">
               <div>
               <div>
-                <label className="block text-sm font-medium text-white mb-2">
+                <label className="block text-sm font-medium text-white mb-1">
                   {t('settings.email.testRecipient') || 'Test Recipient Email'}
                   {t('settings.email.testRecipient') || 'Test Recipient Email'}
                 </label>
                 </label>
                 <input
                 <input
@@ -432,6 +433,7 @@ export function EmailSettings() {
             </div>
             </div>
           </CardContent>
           </CardContent>
         </Card>
         </Card>
+        </div>
       </div>
       </div>
 
 
     </div>
     </div>

+ 144 - 133
frontend/src/components/LDAPSettings.tsx

@@ -6,6 +6,7 @@ import { api } from '../api/client';
 import type { AppSettings } from '../api/client';
 import type { AppSettings } from '../api/client';
 import { Card, CardContent, CardHeader } from './Card';
 import { Card, CardContent, CardHeader } from './Card';
 import { Button } from './Button';
 import { Button } from './Button';
+import { Collapsible } from './Collapsible';
 import { useToast } from '../contexts/ToastContext';
 import { useToast } from '../contexts/ToastContext';
 import { useAuth } from '../contexts/AuthContext';
 import { useAuth } from '../contexts/AuthContext';
 
 
@@ -165,10 +166,10 @@ export function LDAPSettings() {
   }
   }
 
 
   const ldapEnabled = ldapStatus?.ldap_enabled ?? false;
   const ldapEnabled = ldapStatus?.ldap_enabled ?? false;
-  const inputClasses = "w-full px-4 py-3 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-lg text-white placeholder-bambu-gray focus:outline-none focus:ring-2 focus:ring-bambu-green/50 focus:border-bambu-green transition-colors";
+  const inputClasses = "w-full px-3 py-2 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-lg text-white placeholder-bambu-gray focus:outline-none focus:ring-2 focus:ring-bambu-green/50 focus:border-bambu-green transition-colors";
 
 
   return (
   return (
-    <div className="space-y-6">
+    <div className="space-y-3">
       {/* LDAP Toggle */}
       {/* LDAP Toggle */}
       <Card>
       <Card>
         <CardHeader>
         <CardHeader>
@@ -241,148 +242,158 @@ export function LDAPSettings() {
           </h2>
           </h2>
         </CardHeader>
         </CardHeader>
         <CardContent>
         <CardContent>
-          <div className="space-y-4">
-            {/* Server URL */}
-            <div>
-              <label className="block text-sm font-medium text-bambu-gray mb-2">
-                {t('settings.ldap.serverUrl') || 'Server URL'}
-              </label>
-              <input
-                type="text"
-                className={inputClasses}
-                placeholder="ldaps://ldap.example.com:636"
-                value={form.ldap_server_url}
-                onChange={e => setForm({ ...form, ldap_server_url: e.target.value })}
-              />
-              <p className="text-xs text-bambu-gray mt-1">
-                {t('settings.ldap.serverUrlHint') || 'Use ldaps:// for SSL or ldap:// with StartTLS'}
-              </p>
-            </div>
-
-            {/* Security */}
-            <div>
-              <label className="block text-sm font-medium text-bambu-gray mb-2">
-                {t('settings.ldap.security') || 'Security'}
-              </label>
-              <div className="flex gap-2">
-                {(['starttls', 'ldaps'] as const).map(sec => (
-                  <button
-                    key={sec}
-                    onClick={() => setForm({ ...form, ldap_security: sec })}
-                    className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
-                      form.ldap_security === sec
-                        ? 'bg-bambu-green text-black'
-                        : 'bg-bambu-dark-secondary text-bambu-gray hover:text-white border border-bambu-dark-tertiary'
-                    }`}
-                  >
-                    {sec === 'starttls' ? 'StartTLS' : 'LDAPS (SSL)'}
-                  </button>
-                ))}
+          <div className="space-y-3">
+            {/* Server URL + Security (side by side) */}
+            <div className="grid grid-cols-1 md:grid-cols-3 gap-3">
+              <div className="md:col-span-2">
+                <label className="block text-sm font-medium text-bambu-gray mb-1">
+                  {t('settings.ldap.serverUrl') || 'Server URL'}
+                </label>
+                <input
+                  type="text"
+                  className={inputClasses}
+                  placeholder="ldaps://ldap.example.com:636"
+                  value={form.ldap_server_url}
+                  onChange={e => setForm({ ...form, ldap_server_url: e.target.value })}
+                />
+                <p className="text-xs text-bambu-gray mt-1">
+                  {t('settings.ldap.serverUrlHint') || 'Use ldaps:// for SSL or ldap:// with StartTLS'}
+                </p>
+              </div>
+              <div>
+                <label className="block text-sm font-medium text-bambu-gray mb-1">
+                  {t('settings.ldap.security') || 'Security'}
+                </label>
+                <div className="flex gap-2">
+                  {(['starttls', 'ldaps'] as const).map(sec => (
+                    <button
+                      key={sec}
+                      onClick={() => setForm({ ...form, ldap_security: sec })}
+                      className={`flex-1 px-2 py-2 rounded-lg text-sm font-medium transition-colors ${
+                        form.ldap_security === sec
+                          ? 'bg-bambu-green text-black'
+                          : 'bg-bambu-dark-secondary text-bambu-gray hover:text-white border border-bambu-dark-tertiary'
+                      }`}
+                    >
+                      {sec === 'starttls' ? 'StartTLS' : 'LDAPS'}
+                    </button>
+                  ))}
+                </div>
+                <p className="text-xs text-bambu-gray mt-1">
+                  {t('settings.ldap.securityHint') || `Default port: ${SECURITY_PORT_MAP[form.ldap_security]}`}
+                </p>
               </div>
               </div>
-              <p className="text-xs text-bambu-gray mt-1">
-                {t('settings.ldap.securityHint') || `Default port: ${SECURITY_PORT_MAP[form.ldap_security]}`}
-              </p>
-            </div>
-
-            {/* Bind DN */}
-            <div>
-              <label className="block text-sm font-medium text-bambu-gray mb-2">
-                {t('settings.ldap.bindDn') || 'Bind DN (Service Account)'}
-              </label>
-              <input
-                type="text"
-                className={inputClasses}
-                placeholder="cn=service-account,ou=service,dc=example,dc=com"
-                value={form.ldap_bind_dn}
-                onChange={e => setForm({ ...form, ldap_bind_dn: e.target.value })}
-              />
-            </div>
-
-            {/* Bind Password */}
-            <div>
-              <label className="block text-sm font-medium text-bambu-gray mb-2">
-                {t('settings.ldap.bindPassword') || 'Bind Password'}
-              </label>
-              <input
-                type="password"
-                className={inputClasses}
-                placeholder={settings?.ldap_bind_dn ? '••••••••' : ''}
-                value={form.ldap_bind_password}
-                onChange={e => setForm({ ...form, ldap_bind_password: e.target.value })}
-              />
-            </div>
-
-            {/* Search Base */}
-            <div>
-              <label className="block text-sm font-medium text-bambu-gray mb-2">
-                {t('settings.ldap.searchBase') || 'Search Base DN'}
-              </label>
-              <input
-                type="text"
-                className={inputClasses}
-                placeholder="ou=users,dc=example,dc=com"
-                value={form.ldap_search_base}
-                onChange={e => setForm({ ...form, ldap_search_base: e.target.value })}
-              />
             </div>
             </div>
 
 
-            {/* User Filter */}
-            <div>
-              <label className="block text-sm font-medium text-bambu-gray mb-2">
-                {t('settings.ldap.userFilter') || 'User Search Filter'}
-              </label>
-              <input
-                type="text"
-                className={inputClasses}
-                placeholder="(sAMAccountName={username})"
-                value={form.ldap_user_filter}
-                onChange={e => setForm({ ...form, ldap_user_filter: e.target.value })}
-              />
-              <p className="text-xs text-bambu-gray mt-1">
-                {t('settings.ldap.userFilterHint') || '{username} is replaced with the login username. Use (uid={username}) for OpenLDAP.'}
-              </p>
+            {/* Bind DN + Password (side by side) */}
+            <div className="grid grid-cols-1 md:grid-cols-2 gap-3">
+              <div>
+                <label className="block text-sm font-medium text-bambu-gray mb-1">
+                  {t('settings.ldap.bindDn') || 'Bind DN (Service Account)'}
+                </label>
+                <input
+                  type="text"
+                  className={inputClasses}
+                  placeholder="cn=service-account,ou=service,dc=example,dc=com"
+                  value={form.ldap_bind_dn}
+                  onChange={e => setForm({ ...form, ldap_bind_dn: e.target.value })}
+                />
+              </div>
+              <div>
+                <label className="block text-sm font-medium text-bambu-gray mb-1">
+                  {t('settings.ldap.bindPassword') || 'Bind Password'}
+                </label>
+                <input
+                  type="password"
+                  className={inputClasses}
+                  placeholder={settings?.ldap_bind_dn ? '••••••••' : ''}
+                  value={form.ldap_bind_password}
+                  onChange={e => setForm({ ...form, ldap_bind_password: e.target.value })}
+                />
+              </div>
             </div>
             </div>
 
 
-            {/* Auto Provision */}
-            <div className="flex items-center justify-between py-2">
+            {/* Search Base + User Filter (side by side) */}
+            <div className="grid grid-cols-1 md:grid-cols-2 gap-3">
               <div>
               <div>
-                <label className="block text-sm font-medium text-white">
-                  {t('settings.ldap.autoProvision') || 'Auto-provision users'}
+                <label className="block text-sm font-medium text-bambu-gray mb-1">
+                  {t('settings.ldap.searchBase') || 'Search Base DN'}
                 </label>
                 </label>
-                <p className="text-xs text-bambu-gray mt-0.5">
-                  {t('settings.ldap.autoProvisionHint') || 'Automatically create a BamBuddy account on first LDAP login'}
-                </p>
+                <input
+                  type="text"
+                  className={inputClasses}
+                  placeholder="ou=users,dc=example,dc=com"
+                  value={form.ldap_search_base}
+                  onChange={e => setForm({ ...form, ldap_search_base: e.target.value })}
+                />
               </div>
               </div>
-              <button
-                onClick={() => setForm({ ...form, ldap_auto_provision: !form.ldap_auto_provision })}
-                className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${
-                  form.ldap_auto_provision ? 'bg-bambu-green' : 'bg-bambu-dark-tertiary'
-                }`}
-              >
-                <span
-                  className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
-                    form.ldap_auto_provision ? 'translate-x-6' : 'translate-x-1'
-                  }`}
+              <div>
+                <label className="block text-sm font-medium text-bambu-gray mb-1">
+                  {t('settings.ldap.userFilter') || 'User Search Filter'}
+                </label>
+                <input
+                  type="text"
+                  className={inputClasses}
+                  placeholder="(sAMAccountName={username})"
+                  value={form.ldap_user_filter}
+                  onChange={e => setForm({ ...form, ldap_user_filter: e.target.value })}
                 />
                 />
-              </button>
+              </div>
             </div>
             </div>
 
 
-            {/* Group Mapping */}
-            <div>
-              <label className="block text-sm font-medium text-bambu-gray mb-2">
-                {t('settings.ldap.groupMapping') || 'Group Mapping (JSON)'}
-              </label>
-              <textarea
-                className={`${inputClasses} font-mono text-sm`}
-                rows={4}
-                placeholder={'{\n  "CN=PrintFarm_Admins,OU=Groups,DC=example,DC=com": "Administrators",\n  "CN=PrintFarm_Users,OU=Groups,DC=example,DC=com": "Operators"\n}'}
-                value={form.ldap_group_mapping}
-                onChange={e => setForm({ ...form, ldap_group_mapping: e.target.value })}
-              />
-              <p className="text-xs text-bambu-gray mt-1">
-                {t('settings.ldap.groupMappingHint') || 'Map LDAP group DNs to BamBuddy groups. Available groups: '}{groups.map(g => g.name).join(', ')}
-              </p>
-            </div>
+            {/* Advanced (collapsed by default) */}
+            <Collapsible
+              summary={
+                <span className="text-sm font-medium text-bambu-gray">
+                  {t('settings.ldap.advanced') || 'Advanced'}
+                </span>
+              }
+              className="border-t border-bambu-dark-tertiary pt-3"
+              summaryClassName="py-1"
+            >
+              <div className="space-y-3">
+                {/* Auto Provision */}
+                <div className="flex items-center justify-between">
+                  <div>
+                    <label className="block text-sm font-medium text-white">
+                      {t('settings.ldap.autoProvision') || 'Auto-provision users'}
+                    </label>
+                    <p className="text-xs text-bambu-gray mt-0.5">
+                      {t('settings.ldap.autoProvisionHint') || 'Automatically create a BamBuddy account on first LDAP login'}
+                    </p>
+                  </div>
+                  <button
+                    onClick={() => setForm({ ...form, ldap_auto_provision: !form.ldap_auto_provision })}
+                    className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors flex-shrink-0 ${
+                      form.ldap_auto_provision ? 'bg-bambu-green' : 'bg-bambu-dark-tertiary'
+                    }`}
+                  >
+                    <span
+                      className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
+                        form.ldap_auto_provision ? 'translate-x-6' : 'translate-x-1'
+                      }`}
+                    />
+                  </button>
+                </div>
+
+                {/* Group Mapping */}
+                <div>
+                  <label className="block text-sm font-medium text-bambu-gray mb-1">
+                    {t('settings.ldap.groupMapping') || 'Group Mapping (JSON)'}
+                  </label>
+                  <textarea
+                    className={`${inputClasses} font-mono text-sm`}
+                    rows={4}
+                    placeholder={'{\n  "CN=PrintFarm_Admins,OU=Groups,DC=example,DC=com": "Administrators",\n  "CN=PrintFarm_Users,OU=Groups,DC=example,DC=com": "Operators"\n}'}
+                    value={form.ldap_group_mapping}
+                    onChange={e => setForm({ ...form, ldap_group_mapping: e.target.value })}
+                  />
+                  <p className="text-xs text-bambu-gray mt-1">
+                    {t('settings.ldap.groupMappingHint') || 'Map LDAP group DNs to BamBuddy groups. Available groups: '}{groups.map(g => g.name).join(', ')}
+                  </p>
+                </div>
+              </div>
+            </Collapsible>
 
 
             {/* Action Buttons */}
             {/* Action Buttons */}
             <div className="flex gap-3 pt-2">
             <div className="flex gap-3 pt-2">

+ 5 - 3
frontend/src/components/NotificationProviderCard.tsx

@@ -67,9 +67,9 @@ export function NotificationProviderCard({ provider, onEdit }: NotificationProvi
   return (
   return (
     <>
     <>
       <Card className="relative">
       <Card className="relative">
-        <CardContent className="p-4">
+        <CardContent className="p-3">
           {/* Header Row */}
           {/* Header Row */}
-          <div className="flex items-start justify-between mb-3">
+          <div className={`flex items-start justify-between ${provider.enabled ? 'mb-3' : 'mb-0'}`}>
             <div className="flex items-center gap-3">
             <div className="flex items-center gap-3">
               <div className={`p-2 rounded-lg ${provider.enabled ? 'bg-bambu-green/20' : 'bg-bambu-dark'}`}>
               <div className={`p-2 rounded-lg ${provider.enabled ? 'bg-bambu-green/20' : 'bg-bambu-dark'}`}>
                 <Bell className={`w-5 h-5 ${provider.enabled ? 'text-bambu-green' : 'text-bambu-gray'}`} />
                 <Bell className={`w-5 h-5 ${provider.enabled ? 'text-bambu-green' : 'text-bambu-gray'}`} />
@@ -98,6 +98,7 @@ export function NotificationProviderCard({ provider, onEdit }: NotificationProvi
             </div>
             </div>
           </div>
           </div>
 
 
+          {provider.enabled && (<>
           {/* Linked Printer */}
           {/* Linked Printer */}
           {linkedPrinter && (
           {linkedPrinter && (
             <div className="mb-3 px-2 py-1.5 bg-bambu-dark rounded-lg">
             <div className="mb-3 px-2 py-1.5 bg-bambu-dark rounded-lg">
@@ -215,10 +216,11 @@ export function NotificationProviderCard({ provider, onEdit }: NotificationProvi
             </div>
             </div>
           )}
           )}
 
 
+          </>)}
           {/* Toggle Settings Panel */}
           {/* Toggle Settings Panel */}
           <button
           <button
             onClick={() => setIsExpanded(!isExpanded)}
             onClick={() => setIsExpanded(!isExpanded)}
-            className="w-full flex items-center justify-between py-2 text-sm text-bambu-gray hover:text-white transition-colors border-t border-bambu-dark-tertiary"
+            className={`w-full flex items-center justify-between py-2 text-sm text-bambu-gray hover:text-white transition-colors ${provider.enabled ? 'border-t border-bambu-dark-tertiary' : 'mt-2 border-t border-bambu-dark-tertiary'}`}
           >
           >
             <span className="flex items-center gap-2">
             <span className="flex items-center gap-2">
               <Settings2 className="w-4 h-4" />
               <Settings2 className="w-4 h-4" />

+ 102 - 83
frontend/src/pages/SettingsPage.tsx

@@ -7,7 +7,8 @@ import { useAuth } from '../contexts/AuthContext';
 import { formatDateOnly } from '../utils/date';
 import { formatDateOnly } from '../utils/date';
 import { getCurrencySymbol, SUPPORTED_CURRENCIES } from '../utils/currency';
 import { getCurrencySymbol, SUPPORTED_CURRENCIES } from '../utils/currency';
 import type { AppSettings, AppSettingsUpdate, SmartPlug, SmartPlugStatus, NotificationProvider, NotificationTemplate, UpdateStatus, GitHubBackupStatus, CloudAuthStatus, UserCreate, UserUpdate, UserResponse, StorageUsageResponse } from '../api/client';
 import type { AppSettings, AppSettingsUpdate, SmartPlug, SmartPlugStatus, NotificationProvider, NotificationTemplate, UpdateStatus, GitHubBackupStatus, CloudAuthStatus, UserCreate, UserUpdate, UserResponse, StorageUsageResponse } from '../api/client';
-import { Card, CardContent, CardHeader } from '../components/Card';
+import { Card, CardContent, CardDensityProvider, CardHeader } from '../components/Card';
+import { Collapsible } from '../components/Collapsible';
 import { Button } from '../components/Button';
 import { Button } from '../components/Button';
 import { SmartPlugCard } from '../components/SmartPlugCard';
 import { SmartPlugCard } from '../components/SmartPlugCard';
 import { AddSmartPlugModal } from '../components/AddSmartPlugModal';
 import { AddSmartPlugModal } from '../components/AddSmartPlugModal';
@@ -964,14 +965,15 @@ export function SettingsPage() {
   }
   }
 
 
   return (
   return (
-    <div className="p-4 md:p-8">
-      <div className="mb-8">
+    <CardDensityProvider density="dense">
+    <div className="p-4 md:p-6">
+      <div className="mb-4 flex items-baseline gap-3">
         <h1 className="text-2xl font-bold text-white">{t('settings.title')}</h1>
         <h1 className="text-2xl font-bold text-white">{t('settings.title')}</h1>
-        <p className="text-bambu-gray">{t('settings.configureBambuddy')}</p>
+        <p className="text-sm text-bambu-gray hidden sm:block">{t('settings.configureBambuddy')}</p>
       </div>
       </div>
 
 
       {/* Tab Navigation */}
       {/* Tab Navigation */}
-      <div className="flex flex-wrap gap-1 mb-6 border-b border-bambu-dark-tertiary">
+      <div className="flex flex-wrap gap-1 mb-4 border-b border-bambu-dark-tertiary">
         <button
         <button
           onClick={() => handleTabChange('general')}
           onClick={() => handleTabChange('general')}
           className={`px-4 py-2 text-sm font-medium transition-colors border-b-2 -mb-px ${
           className={`px-4 py-2 text-sm font-medium transition-colors border-b-2 -mb-px ${
@@ -1104,14 +1106,14 @@ export function SettingsPage() {
         </button>
         </button>
       </div>
       </div>
       {activeTab === 'general' && (
       {activeTab === 'general' && (
-      <div className="flex flex-col lg:flex-row gap-6 lg:gap-8">
+      <div className="flex flex-col lg:flex-row gap-4 lg:gap-6">
         {/* Left Column - General Settings */}
         {/* Left Column - General Settings */}
-        <div className="space-y-6 flex-1 lg:max-w-xl">
+        <div className="space-y-3 flex-1 lg:max-w-xl">
           <Card>
           <Card>
             <CardHeader>
             <CardHeader>
               <h2 className="text-lg font-semibold text-white">{t('settings.general')}</h2>
               <h2 className="text-lg font-semibold text-white">{t('settings.general')}</h2>
             </CardHeader>
             </CardHeader>
-            <CardContent className="space-y-4">
+            <CardContent className="space-y-3">
               <div>
               <div>
                 <label className="block text-sm text-bambu-gray mb-1">
                 <label className="block text-sm text-bambu-gray mb-1">
                   <Globe className="w-4 h-4 inline mr-1" />
                   <Globe className="w-4 h-4 inline mr-1" />
@@ -1275,7 +1277,7 @@ export function SettingsPage() {
                 {t('settings.appearance')}
                 {t('settings.appearance')}
               </h2>
               </h2>
             </CardHeader>
             </CardHeader>
-            <CardContent className="space-y-6">
+            <CardContent className="space-y-3">
               {/* Dark Mode Settings */}
               {/* Dark Mode Settings */}
               <div className={`space-y-3 p-4 rounded-lg border ${mode === 'dark' ? 'border-bambu-green bg-bambu-green/5' : 'border-bambu-dark-tertiary'}`}>
               <div className={`space-y-3 p-4 rounded-lg border ${mode === 'dark' ? 'border-bambu-green bg-bambu-green/5' : 'border-bambu-dark-tertiary'}`}>
                 <h3 className="text-sm font-medium text-white flex items-center gap-2">
                 <h3 className="text-sm font-medium text-white flex items-center gap-2">
@@ -1387,7 +1389,7 @@ export function SettingsPage() {
             <CardHeader>
             <CardHeader>
               <h2 className="text-lg font-semibold text-white">{t('settings.archiveSettings')}</h2>
               <h2 className="text-lg font-semibold text-white">{t('settings.archiveSettings')}</h2>
             </CardHeader>
             </CardHeader>
-            <CardContent className="space-y-4">
+            <CardContent className="space-y-3">
               <div className="flex items-center justify-between">
               <div className="flex items-center justify-between">
                 <div>
                 <div>
                   <p className="text-white">{t('settings.autoArchivePrints')}</p>
                   <p className="text-white">{t('settings.autoArchivePrints')}</p>
@@ -1456,7 +1458,7 @@ export function SettingsPage() {
         </div>
         </div>
 
 
         {/* Second Column - Camera, Cost, AMS & Spoolman */}
         {/* Second Column - Camera, Cost, AMS & Spoolman */}
-        <div className="space-y-6 flex-1 lg:max-w-md">
+        <div className="space-y-3 flex-1 lg:max-w-md">
           {/* Camera Settings */}
           {/* Camera Settings */}
           <Card>
           <Card>
             <CardHeader>
             <CardHeader>
@@ -1465,7 +1467,7 @@ export function SettingsPage() {
                 {t('settings.camera')}
                 {t('settings.camera')}
               </h2>
               </h2>
             </CardHeader>
             </CardHeader>
-            <CardContent className="space-y-4">
+            <CardContent className="space-y-3">
               <div>
               <div>
                 <label className="block text-sm text-bambu-gray mb-1">
                 <label className="block text-sm text-bambu-gray mb-1">
                   {t('settings.cameraViewMode')}
                   {t('settings.cameraViewMode')}
@@ -1587,7 +1589,7 @@ export function SettingsPage() {
             <CardHeader>
             <CardHeader>
               <h2 className="text-lg font-semibold text-white">{t('settings.costTracking')}</h2>
               <h2 className="text-lg font-semibold text-white">{t('settings.costTracking')}</h2>
             </CardHeader>
             </CardHeader>
-            <CardContent className="space-y-4">
+            <CardContent className="space-y-3">
               <div>
               <div>
                 <label className="block text-sm text-bambu-gray mb-1">{t('settings.currency')}</label>
                 <label className="block text-sm text-bambu-gray mb-1">{t('settings.currency')}</label>
                 <select
                 <select
@@ -1671,7 +1673,7 @@ export function SettingsPage() {
                 {t('settings.fileManager')}
                 {t('settings.fileManager')}
               </h2>
               </h2>
             </CardHeader>
             </CardHeader>
-            <CardContent className="space-y-4">
+            <CardContent className="space-y-3">
               {/* Archive Mode */}
               {/* Archive Mode */}
               <div>
               <div>
                 <label className="block text-sm text-bambu-gray mb-1">
                 <label className="block text-sm text-bambu-gray mb-1">
@@ -1717,7 +1719,7 @@ export function SettingsPage() {
         </div>
         </div>
 
 
         {/* Third Column - Sidebar Links & Updates */}
         {/* Third Column - Sidebar Links & Updates */}
-        <div className="space-y-6 flex-1 lg:max-w-sm">
+        <div className="space-y-3 flex-1 lg:max-w-sm">
           {/* Sidebar Links */}
           {/* Sidebar Links */}
           <ExternalLinksSettings />
           <ExternalLinksSettings />
 
 
@@ -1725,7 +1727,7 @@ export function SettingsPage() {
             <CardHeader>
             <CardHeader>
               <h2 className="text-lg font-semibold text-white">{t('settings.updates')}</h2>
               <h2 className="text-lg font-semibold text-white">{t('settings.updates')}</h2>
             </CardHeader>
             </CardHeader>
-            <CardContent className="space-y-4">
+            <CardContent className="space-y-3">
               <p className="text-xs font-medium text-bambu-gray uppercase tracking-wider">{t('settings.printerFirmware')}</p>
               <p className="text-xs font-medium text-bambu-gray uppercase tracking-wider">{t('settings.printerFirmware')}</p>
               <div className="flex items-center justify-between">
               <div className="flex items-center justify-between">
                 <div>
                 <div>
@@ -1900,7 +1902,7 @@ export function SettingsPage() {
             <CardHeader>
             <CardHeader>
               <h2 className="text-lg font-semibold text-white">{t('settings.dataManagement')}</h2>
               <h2 className="text-lg font-semibold text-white">{t('settings.dataManagement')}</h2>
             </CardHeader>
             </CardHeader>
-            <CardContent className="space-y-4">
+            <CardContent className="space-y-3">
               <div className="flex items-center justify-between">
               <div className="flex items-center justify-between">
                 <div>
                 <div>
                   <p className="text-white">{t('settings.clearNotificationLogs')}</p>
                   <p className="text-white">{t('settings.clearNotificationLogs')}</p>
@@ -2060,7 +2062,7 @@ export function SettingsPage() {
       {activeTab === 'network' && localSettings && (
       {activeTab === 'network' && localSettings && (
       <div className="flex flex-col lg:flex-row gap-6">
       <div className="flex flex-col lg:flex-row gap-6">
         {/* Left Column - External URL & FTP Retry */}
         {/* Left Column - External URL & FTP Retry */}
-        <div className="flex-1 lg:max-w-xl space-y-4">
+        <div className="flex-1 lg:max-w-xl space-y-3">
           {/* External URL */}
           {/* External URL */}
           <Card>
           <Card>
             <CardHeader>
             <CardHeader>
@@ -2069,7 +2071,7 @@ export function SettingsPage() {
                 {t('settings.externalUrl')}
                 {t('settings.externalUrl')}
               </h2>
               </h2>
             </CardHeader>
             </CardHeader>
-            <CardContent className="space-y-4">
+            <CardContent className="space-y-3">
               <p className="text-sm text-bambu-gray">
               <p className="text-sm text-bambu-gray">
                 {t('settings.externalUrlDescription')}
                 {t('settings.externalUrlDescription')}
               </p>
               </p>
@@ -2098,7 +2100,7 @@ export function SettingsPage() {
                 {t('settings.ftpRetry')}
                 {t('settings.ftpRetry')}
               </h2>
               </h2>
             </CardHeader>
             </CardHeader>
-            <CardContent className="space-y-4">
+            <CardContent className="space-y-3">
               <p className="text-sm text-bambu-gray">
               <p className="text-sm text-bambu-gray">
                 {t('settings.ftpRetryDescription')}
                 {t('settings.ftpRetryDescription')}
               </p>
               </p>
@@ -2122,7 +2124,7 @@ export function SettingsPage() {
               </div>
               </div>
 
 
               {localSettings.ftp_retry_enabled && (
               {localSettings.ftp_retry_enabled && (
-                <div className="space-y-4 pt-2 border-t border-bambu-dark-tertiary">
+                <div className="space-y-3 pt-2 border-t border-bambu-dark-tertiary">
                   <div>
                   <div>
                     <label className="block text-sm text-bambu-gray mb-1">
                     <label className="block text-sm text-bambu-gray mb-1">
                       {t('settings.retryAttempts')}
                       {t('settings.retryAttempts')}
@@ -2186,7 +2188,7 @@ export function SettingsPage() {
         </div>
         </div>
 
 
         {/* Right Column - Home Assistant & MQTT Publishing */}
         {/* Right Column - Home Assistant & MQTT Publishing */}
-        <div className="flex-1 lg:max-w-xl space-y-4">
+        <div className="flex-1 lg:max-w-xl space-y-3">
           {/* Home Assistant Integration */}
           {/* Home Assistant Integration */}
           <Card>
           <Card>
             <CardHeader>
             <CardHeader>
@@ -2205,7 +2207,7 @@ export function SettingsPage() {
                 )}
                 )}
               </div>
               </div>
             </CardHeader>
             </CardHeader>
-            <CardContent className="space-y-4">
+            <CardContent className="space-y-3">
               <p className="text-sm text-bambu-gray">
               <p className="text-sm text-bambu-gray">
                 {t('settings.homeAssistantFullDescription')}
                 {t('settings.homeAssistantFullDescription')}
               </p>
               </p>
@@ -2352,7 +2354,7 @@ export function SettingsPage() {
                 )}
                 )}
               </div>
               </div>
             </CardHeader>
             </CardHeader>
-            <CardContent className="space-y-4">
+            <CardContent className="space-y-3">
               <p className="text-sm text-bambu-gray">
               <p className="text-sm text-bambu-gray">
                 {t('settings.mqttDescription')}
                 {t('settings.mqttDescription')}
               </p>
               </p>
@@ -2376,7 +2378,7 @@ export function SettingsPage() {
               </div>
               </div>
 
 
               {localSettings.mqtt_enabled && (
               {localSettings.mqtt_enabled && (
-                <div className="space-y-4 pt-2 border-t border-bambu-dark-tertiary">
+                <div className="space-y-3 pt-2 border-t border-bambu-dark-tertiary">
                   <div>
                   <div>
                     <label className="block text-sm text-bambu-gray mb-1">
                     <label className="block text-sm text-bambu-gray mb-1">
                       {t('settings.brokerHostname')}
                       {t('settings.brokerHostname')}
@@ -2492,7 +2494,7 @@ export function SettingsPage() {
         </div>
         </div>
 
 
         {/* Third Column - Prometheus Metrics */}
         {/* Third Column - Prometheus Metrics */}
-        <div className="flex-1 lg:max-w-md space-y-4">
+        <div className="flex-1 lg:max-w-md space-y-3">
           <Card>
           <Card>
             <CardHeader>
             <CardHeader>
               <h2 className="text-lg font-semibold text-white flex items-center gap-2">
               <h2 className="text-lg font-semibold text-white flex items-center gap-2">
@@ -2500,7 +2502,7 @@ export function SettingsPage() {
                 {t('settings.prometheusMetrics')}
                 {t('settings.prometheusMetrics')}
               </h2>
               </h2>
             </CardHeader>
             </CardHeader>
-            <CardContent className="space-y-4">
+            <CardContent className="space-y-3">
               <p className="text-sm text-bambu-gray">
               <p className="text-sm text-bambu-gray">
                 {t('settings.prometheusEndpointDescription')}
                 {t('settings.prometheusEndpointDescription')}
               </p>
               </p>
@@ -2522,7 +2524,7 @@ export function SettingsPage() {
               </div>
               </div>
 
 
               {localSettings.prometheus_enabled && (
               {localSettings.prometheus_enabled && (
-                <div className="space-y-4 pt-2 border-t border-bambu-dark-tertiary">
+                <div className="space-y-3 pt-2 border-t border-bambu-dark-tertiary">
                   <div>
                   <div>
                     <label className="block text-sm text-bambu-gray mb-1">
                     <label className="block text-sm text-bambu-gray mb-1">
                       {t('settings.bearerTokenOptional')}
                       {t('settings.bearerTokenOptional')}
@@ -2592,7 +2594,7 @@ export function SettingsPage() {
 
 
       {/* Smart Plugs Tab */}
       {/* Smart Plugs Tab */}
       {activeTab === 'plugs' && (
       {activeTab === 'plugs' && (
-        <div className="max-w-4xl">
+        <div>
           <div className="flex items-start justify-between mb-6">
           <div className="flex items-start justify-between mb-6">
             <div>
             <div>
               <h2 className="text-lg font-semibold text-white flex items-center gap-2">
               <h2 className="text-lg font-semibold text-white flex items-center gap-2">
@@ -2746,7 +2748,7 @@ export function SettingsPage() {
               <Loader2 className="w-8 h-8 text-bambu-green animate-spin" />
               <Loader2 className="w-8 h-8 text-bambu-green animate-spin" />
             </div>
             </div>
           ) : smartPlugs && smartPlugs.length > 0 ? (
           ) : smartPlugs && smartPlugs.length > 0 ? (
-            <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
+            <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
               {smartPlugs.map((plug) => (
               {smartPlugs.map((plug) => (
                 <SmartPlugCard
                 <SmartPlugCard
                   key={plug.id}
                   key={plug.id}
@@ -2783,7 +2785,7 @@ export function SettingsPage() {
 
 
       {/* Notifications Tab */}
       {/* Notifications Tab */}
       {activeTab === 'notifications' && (
       {activeTab === 'notifications' && (
-        <div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
+        <div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
           {/* Left Column: Providers */}
           {/* Left Column: Providers */}
           <div>
           <div>
             <div className="flex items-center justify-between mb-4">
             <div className="flex items-center justify-between mb-4">
@@ -3042,7 +3044,7 @@ export function SettingsPage() {
 
 
       {/* API Keys Tab */}
       {/* API Keys Tab */}
       {activeTab === 'apikeys' && (
       {activeTab === 'apikeys' && (
-        <div className="grid grid-cols-1 xl:grid-cols-2 gap-8">
+        <div className="grid grid-cols-1 xl:grid-cols-2 gap-4">
           {/* Left Column - API Keys Management */}
           {/* Left Column - API Keys Management */}
           <div>
           <div>
             <div className="flex items-start justify-between gap-4 mb-6">
             <div className="flex items-start justify-between gap-4 mb-6">
@@ -3133,7 +3135,7 @@ export function SettingsPage() {
                 <CardHeader>
                 <CardHeader>
                   <h3 className="text-base font-semibold text-white">{t('settings.createNewApiKey')}</h3>
                   <h3 className="text-base font-semibold text-white">{t('settings.createNewApiKey')}</h3>
                 </CardHeader>
                 </CardHeader>
-                <CardContent className="space-y-4">
+                <CardContent className="space-y-3">
                   <div>
                   <div>
                     <label className="block text-sm text-bambu-gray mb-1">{t('settings.keyName')}</label>
                     <label className="block text-sm text-bambu-gray mb-1">{t('settings.keyName')}</label>
                     <input
                     <input
@@ -3357,9 +3359,9 @@ export function SettingsPage() {
       {/* Filament Tab */}
       {/* Filament Tab */}
       {/* Queue Tab */}
       {/* Queue Tab */}
       {activeTab === 'queue' && localSettings && (
       {activeTab === 'queue' && localSettings && (
-        <div className="flex flex-col lg:flex-row gap-6 lg:gap-8">
+        <div className="flex flex-col lg:flex-row gap-4 lg:gap-6">
           {/* Left Column */}
           {/* Left Column */}
-          <div className="lg:w-1/2 space-y-6">
+          <div className="lg:w-1/2 space-y-3">
           {/* Default Print Options */}
           {/* Default Print Options */}
           <Card>
           <Card>
             <CardHeader>
             <CardHeader>
@@ -3368,7 +3370,7 @@ export function SettingsPage() {
                 {t('settings.defaultPrintOptions', 'Default Print Options')}
                 {t('settings.defaultPrintOptions', 'Default Print Options')}
               </h3>
               </h3>
             </CardHeader>
             </CardHeader>
-            <CardContent className="space-y-4">
+            <CardContent className="space-y-3">
               <p className="text-xs text-bambu-gray">
               <p className="text-xs text-bambu-gray">
                 {t('settings.defaultPrintOptionsDescription', 'Set default values for print options when starting new prints. These can be overridden per print in the print dialog.')}
                 {t('settings.defaultPrintOptionsDescription', 'Set default values for print options when starting new prints. These can be overridden per print in the print dialog.')}
               </p>
               </p>
@@ -3406,7 +3408,7 @@ export function SettingsPage() {
                 {t('settings.staggeredStart', 'Staggered Start')}
                 {t('settings.staggeredStart', 'Staggered Start')}
               </h3>
               </h3>
             </CardHeader>
             </CardHeader>
-            <CardContent className="space-y-4">
+            <CardContent className="space-y-3">
               <p className="text-xs text-bambu-gray">
               <p className="text-xs text-bambu-gray">
                 {t('settings.staggeredStartDescription', 'Default group size and interval when staggering multi-printer batch starts. Can be overridden per batch in the print modal.')}
                 {t('settings.staggeredStartDescription', 'Default group size and interval when staggering multi-printer batch starts. Can be overridden per batch in the print modal.')}
               </p>
               </p>
@@ -3455,7 +3457,7 @@ export function SettingsPage() {
                 {t('settings.plateClear', 'Plate-Clear Confirmation')}
                 {t('settings.plateClear', 'Plate-Clear Confirmation')}
               </h3>
               </h3>
             </CardHeader>
             </CardHeader>
-            <CardContent className="space-y-4">
+            <CardContent className="space-y-3">
               <div className="flex items-center justify-between">
               <div className="flex items-center justify-between">
                 <div className="flex-1 mr-4">
                 <div className="flex-1 mr-4">
                   <p className="text-sm text-white">
                   <p className="text-sm text-white">
@@ -3486,7 +3488,7 @@ export function SettingsPage() {
                 {t('settings.gcodeInjection', 'G-code Injection')}
                 {t('settings.gcodeInjection', 'G-code Injection')}
               </h3>
               </h3>
             </CardHeader>
             </CardHeader>
-            <CardContent className="space-y-4">
+            <CardContent className="space-y-3">
               <p className="text-xs text-bambu-gray">
               <p className="text-xs text-bambu-gray">
                 {t('settings.gcodeInjectionDescription', 'Configure custom G-code to inject at the start and/or end of prints for auto-print systems like Farmloop, SwapMod, AutoClear, and Printflow 3D. Snippets are configured per printer model and applied when "Inject G-code" is enabled on a queue item.')}
                 {t('settings.gcodeInjectionDescription', 'Configure custom G-code to inject at the start and/or end of prints for auto-print systems like Farmloop, SwapMod, AutoClear, and Printflow 3D. Snippets are configured per printer model and applied when "Inject G-code" is enabled on a queue item.')}
               </p>
               </p>
@@ -3533,36 +3535,52 @@ export function SettingsPage() {
 
 
                 return printerModels.map((model) => {
                 return printerModels.map((model) => {
                   const snippet = gcodeSnippets[model] || { start_gcode: '', end_gcode: '' };
                   const snippet = gcodeSnippets[model] || { start_gcode: '', end_gcode: '' };
+                  const hasContent = !!(snippet.start_gcode || snippet.end_gcode);
                   return (
                   return (
-                    <div key={model} className="space-y-2">
-                      <h4 className="text-sm font-medium text-white">{model}</h4>
-                      <div>
-                        <label className="block text-xs text-bambu-gray mb-1">
-                          {t('settings.gcodeStartLabel', 'Start G-code')}
-                        </label>
-                        <textarea
-                          value={snippet.start_gcode}
-                          onChange={(e) => updateSnippet(model, 'start_gcode', e.target.value)}
-                          onBlur={saveGcodeSnippets}
-                          placeholder={t('settings.gcodeStartPlaceholder', 'G-code prepended before the print starts...')}
-                          rows={3}
-                          className="w-full px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white text-xs font-mono focus:outline-none focus:border-bambu-green resize-y"
-                        />
-                      </div>
-                      <div>
-                        <label className="block text-xs text-bambu-gray mb-1">
-                          {t('settings.gcodeEndLabel', 'End G-code')}
-                        </label>
-                        <textarea
-                          value={snippet.end_gcode}
-                          onChange={(e) => updateSnippet(model, 'end_gcode', e.target.value)}
-                          onBlur={saveGcodeSnippets}
-                          placeholder={t('settings.gcodeEndPlaceholder', 'G-code appended after the print ends...')}
-                          rows={3}
-                          className="w-full px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white text-xs font-mono focus:outline-none focus:border-bambu-green resize-y"
-                        />
+                    <Collapsible
+                      key={model}
+                      defaultOpen={hasContent}
+                      className="border border-bambu-dark-tertiary rounded-lg px-3 py-2"
+                      summary={
+                        <div className="flex items-center gap-2">
+                          <h4 className="text-sm font-medium text-white">{model}</h4>
+                          {hasContent && (
+                            <span className="text-xs px-1.5 py-0.5 rounded bg-bambu-green/20 text-bambu-green">
+                              {t('settings.gcodeConfigured', 'Configured')}
+                            </span>
+                          )}
+                        </div>
+                      }
+                    >
+                      <div className="space-y-2">
+                        <div>
+                          <label className="block text-xs text-bambu-gray mb-1">
+                            {t('settings.gcodeStartLabel', 'Start G-code')}
+                          </label>
+                          <textarea
+                            value={snippet.start_gcode}
+                            onChange={(e) => updateSnippet(model, 'start_gcode', e.target.value)}
+                            onBlur={saveGcodeSnippets}
+                            placeholder={t('settings.gcodeStartPlaceholder', 'G-code prepended before the print starts...')}
+                            rows={3}
+                            className="w-full px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white text-xs font-mono focus:outline-none focus:border-bambu-green resize-y"
+                          />
+                        </div>
+                        <div>
+                          <label className="block text-xs text-bambu-gray mb-1">
+                            {t('settings.gcodeEndLabel', 'End G-code')}
+                          </label>
+                          <textarea
+                            value={snippet.end_gcode}
+                            onChange={(e) => updateSnippet(model, 'end_gcode', e.target.value)}
+                            onBlur={saveGcodeSnippets}
+                            placeholder={t('settings.gcodeEndPlaceholder', 'G-code appended after the print ends...')}
+                            rows={3}
+                            className="w-full px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white text-xs font-mono focus:outline-none focus:border-bambu-green resize-y"
+                          />
+                        </div>
                       </div>
                       </div>
-                    </div>
+                    </Collapsible>
                   );
                   );
                 });
                 });
               })()}
               })()}
@@ -3571,7 +3589,7 @@ export function SettingsPage() {
 
 
           </div>
           </div>
           {/* Right Column */}
           {/* Right Column */}
-          <div className="lg:w-1/2 space-y-6">
+          <div className="lg:w-1/2 space-y-3">
           {/* Auto-Drying */}
           {/* Auto-Drying */}
           <Card>
           <Card>
             <CardHeader>
             <CardHeader>
@@ -3580,7 +3598,7 @@ export function SettingsPage() {
                 {t('settings.queueDrying')}
                 {t('settings.queueDrying')}
               </h3>
               </h3>
             </CardHeader>
             </CardHeader>
-            <CardContent className="space-y-4">
+            <CardContent className="space-y-3">
               <p className="text-xs text-bambu-gray">
               <p className="text-xs text-bambu-gray">
                 {t('settings.queueDryingDescription')}
                 {t('settings.queueDryingDescription')}
               </p>
               </p>
@@ -3737,16 +3755,16 @@ export function SettingsPage() {
 
 
       {activeTab === 'filament' && localSettings && (
       {activeTab === 'filament' && localSettings && (
         <>
         <>
-        <div className="flex flex-col lg:flex-row gap-6 lg:gap-8">
+        <div className="flex flex-col lg:flex-row gap-4 lg:gap-6">
           {/* Left Column (1/3) - Mode Selector + AMS Thresholds */}
           {/* Left Column (1/3) - Mode Selector + AMS Thresholds */}
-          <div className="lg:w-1/3 space-y-6">
+          <div className="lg:w-1/3 space-y-3">
             <SpoolmanSettings />
             <SpoolmanSettings />
 
 
             <Card>
             <Card>
               <CardHeader>
               <CardHeader>
                 <h2 className="text-lg font-semibold text-white">{t('settings.filamentChecks')}</h2>
                 <h2 className="text-lg font-semibold text-white">{t('settings.filamentChecks')}</h2>
               </CardHeader>
               </CardHeader>
-              <CardContent className="space-y-4">
+              <CardContent className="space-y-3">
                 <div className="flex items-center justify-between">
                 <div className="flex items-center justify-between">
                   <div>
                   <div>
                     <p className="text-white">{t('settings.disableFilamentWarnings')}</p>
                     <p className="text-white">{t('settings.disableFilamentWarnings')}</p>
@@ -3789,7 +3807,7 @@ export function SettingsPage() {
               <CardHeader>
               <CardHeader>
                 <h2 className="text-lg font-semibold text-white">{t('settings.printModal')}</h2>
                 <h2 className="text-lg font-semibold text-white">{t('settings.printModal')}</h2>
               </CardHeader>
               </CardHeader>
-              <CardContent className="space-y-4">
+              <CardContent className="space-y-3">
                 <div className="flex items-center justify-between">
                 <div className="flex items-center justify-between">
                   <div>
                   <div>
                     <p className="text-white">{t('settings.expandCustomMapping')}</p>
                     <p className="text-white">{t('settings.expandCustomMapping')}</p>
@@ -3814,7 +3832,7 @@ export function SettingsPage() {
               <CardHeader>
               <CardHeader>
                 <h2 className="text-lg font-semibold text-white">{t('settings.amsDisplayThresholds')}</h2>
                 <h2 className="text-lg font-semibold text-white">{t('settings.amsDisplayThresholds')}</h2>
               </CardHeader>
               </CardHeader>
-              <CardContent className="space-y-4">
+              <CardContent className="space-y-3">
                 <p className="text-sm text-bambu-gray">
                 <p className="text-sm text-bambu-gray">
                   {t('settings.amsThresholdsDescription')}
                   {t('settings.amsThresholdsDescription')}
                 </p>
                 </p>
@@ -3946,7 +3964,7 @@ export function SettingsPage() {
           </div>
           </div>
 
 
           {/* Right Column (2/3) - Spool Catalog + Color Catalog */}
           {/* Right Column (2/3) - Spool Catalog + Color Catalog */}
-          <div className="lg:w-2/3 space-y-6">
+          <div className="lg:w-2/3 space-y-3">
             <SpoolCatalogSettings />
             <SpoolCatalogSettings />
             <ColorCatalogSettings />
             <ColorCatalogSettings />
           </div>
           </div>
@@ -4114,7 +4132,7 @@ export function SettingsPage() {
 
 
       {/* Users Tab */}
       {/* Users Tab */}
       {activeTab === 'users' && (
       {activeTab === 'users' && (
-        <div className="space-y-6">
+        <div className="space-y-3">
           {/* Sub-tab Navigation */}
           {/* Sub-tab Navigation */}
           <div className="flex gap-1 border-b border-bambu-dark-tertiary">
           <div className="flex gap-1 border-b border-bambu-dark-tertiary">
             <button
             <button
@@ -4219,7 +4237,7 @@ export function SettingsPage() {
           {authEnabled && (
           {authEnabled && (
             <div className="grid grid-cols-1 xl:grid-cols-2 gap-6">
             <div className="grid grid-cols-1 xl:grid-cols-2 gap-6">
               {/* Left Column: Current User + User List */}
               {/* Left Column: Current User + User List */}
-              <div className="space-y-6">
+              <div className="space-y-3">
                 {/* Current User Card */}
                 {/* Current User Card */}
                 {user && (
                 {user && (
                   <Card>
                   <Card>
@@ -4472,13 +4490,13 @@ export function SettingsPage() {
 
 
           {/* Email Auth Sub-tab */}
           {/* Email Auth Sub-tab */}
           {usersSubTab === 'email' && (
           {usersSubTab === 'email' && (
-            <div className="max-w-2xl">
+            <div className="max-w-5xl">
               <EmailSettings />
               <EmailSettings />
             </div>
             </div>
           )}
           )}
 
 
           {usersSubTab === 'ldap' && (
           {usersSubTab === 'ldap' && (
-            <div className="max-w-2xl">
+            <div className="max-w-5xl">
               <LDAPSettings />
               <LDAPSettings />
             </div>
             </div>
           )}
           )}
@@ -4517,7 +4535,7 @@ export function SettingsPage() {
               </div>
               </div>
             </CardHeader>
             </CardHeader>
             <CardContent>
             <CardContent>
-              <div className="space-y-4">
+              <div className="space-y-3">
                 <div>
                 <div>
                   <label className="block text-sm font-medium text-white mb-2">{t('settings.username')}</label>
                   <label className="block text-sm font-medium text-white mb-2">{t('settings.username')}</label>
                   <input
                   <input
@@ -4668,7 +4686,7 @@ export function SettingsPage() {
               </div>
               </div>
             </CardHeader>
             </CardHeader>
             <CardContent>
             <CardContent>
-              <div className="space-y-4">
+              <div className="space-y-3">
                 {/* Username Field */}
                 {/* Username Field */}
                 <div>
                 <div>
                   <label className="block text-sm font-medium text-white mb-2">
                   <label className="block text-sm font-medium text-white mb-2">
@@ -4849,7 +4867,7 @@ export function SettingsPage() {
                 <h3 className="text-lg font-semibold">{t('settings.deleteUserTitle')}</h3>
                 <h3 className="text-lg font-semibold">{t('settings.deleteUserTitle')}</h3>
               </div>
               </div>
             </CardHeader>
             </CardHeader>
-            <CardContent className="space-y-4">
+            <CardContent className="space-y-3">
               {deleteUserLoading ? (
               {deleteUserLoading ? (
                 <div className="flex items-center justify-center py-4">
                 <div className="flex items-center justify-center py-4">
                   <div className="animate-spin rounded-full h-6 w-6 border-2 border-bambu-green border-t-transparent" />
                   <div className="animate-spin rounded-full h-6 w-6 border-2 border-bambu-green border-t-transparent" />
@@ -5005,7 +5023,7 @@ export function SettingsPage() {
               </div>
               </div>
             </CardHeader>
             </CardHeader>
             <CardContent>
             <CardContent>
-              <div className="space-y-4">
+              <div className="space-y-3">
                 <div>
                 <div>
                   <label className="block text-sm font-medium text-white mb-2">
                   <label className="block text-sm font-medium text-white mb-2">
                     Current Password
                     Current Password
@@ -5108,5 +5126,6 @@ export function SettingsPage() {
         </div>
         </div>
       )}
       )}
     </div>
     </div>
+    </CardDensityProvider>
   );
   );
 }
 }

File diff suppressed because it is too large
+ 0 - 0
static/assets/index-BKDWdPbe.css


File diff suppressed because it is too large
+ 0 - 0
static/assets/index-BUj9NFXp.js


File diff suppressed because it is too large
+ 0 - 0
static/assets/index-CA19cW_f.css


+ 2 - 2
static/index.html

@@ -23,8 +23,8 @@
 
 
     <!-- Splash screens for iOS -->
     <!-- Splash screens for iOS -->
     <link rel="apple-touch-startup-image" href="/img/android-chrome-512x512.png" />
     <link rel="apple-touch-startup-image" href="/img/android-chrome-512x512.png" />
-    <script type="module" crossorigin src="/assets/index-BluaTs0d.js"></script>
-    <link rel="stylesheet" crossorigin href="/assets/index-CA19cW_f.css">
+    <script type="module" crossorigin src="/assets/index-BUj9NFXp.js"></script>
+    <link rel="stylesheet" crossorigin href="/assets/index-BKDWdPbe.css">
   </head>
   </head>
   <body>
   <body>
     <div id="root"></div>
     <div id="root"></div>

Some files were not shown because too many files changed in this diff