2
0
Эх сурвалжийг харах

Collapse Swimlane, List, Opened Card. Opened Card window X and Y position can be moved freely from drag handle. Fix some dragging not possible. Fix iPhone Safari.

Thanks to xet7 !

Fixes #6040,
fixes #6027,
fixes #6021,
fixes #6002
Lauri Ojansivu 12 цаг өмнө
parent
commit
58f4884ad6
37 өөрчлөгдсөн 1415 нэмэгдсэн , 112 устгасан
  1. 8 0
      client/00-startup.js
  2. 53 0
      client/components/boards/boardBody.css
  3. 4 0
      client/components/boards/boardBody.jade
  4. 19 0
      client/components/boards/boardBody.js
  5. 22 6
      client/components/boards/boardsList.css
  6. 2 3
      client/components/cards/cardCustomFields.jade
  7. 1 0
      client/components/cards/cardCustomFields.js
  8. 247 7
      client/components/cards/cardDetails.css
  9. 29 4
      client/components/cards/cardDetails.jade
  10. 121 2
      client/components/cards/cardDetails.js
  11. 51 1
      client/components/cards/checklists.css
  12. 2 3
      client/components/cards/checklists.jade
  13. 2 1
      client/components/cards/checklists.js
  14. 28 0
      client/components/cards/subtasks.css
  15. 2 2
      client/components/cards/subtasks.jade
  16. 13 1
      client/components/cards/subtasks.js
  17. 7 0
      client/components/forms/forms.css
  18. 76 31
      client/components/lists/list.css
  19. 3 2
      client/components/lists/list.jade
  20. 11 3
      client/components/lists/list.js
  21. 21 21
      client/components/lists/listHeader.jade
  22. 4 3
      client/components/lists/listHeader.js
  23. 11 5
      client/components/main/header.css
  24. 55 0
      client/components/main/layouts.css
  25. 3 1
      client/components/main/layouts.jade
  26. 29 0
      client/components/main/popup.css
  27. 7 2
      client/components/settings/settingBody.css
  28. 13 0
      client/components/sidebar/sidebar.css
  29. 5 0
      client/components/swimlanes/swimlaneHeader.jade
  30. 8 3
      client/components/swimlanes/swimlaneHeader.js
  31. 23 0
      client/components/swimlanes/swimlanes.css
  32. 49 4
      client/components/swimlanes/swimlanes.js
  33. 121 6
      client/lib/utils.js
  34. 10 0
      config/router.js
  35. 17 0
      models/lists.js
  36. 15 0
      models/swimlanes.js
  37. 323 1
      models/users.js

+ 8 - 0
client/00-startup.js

@@ -70,4 +70,12 @@ Meteor.startup(() => {
       Meteor.subscribe('userGreyIcons');
     }
   });
+  
+  // Initialize mobile mode on startup for iOS devices
+  // This ensures mobile mode is applied correctly on page load
+  Tracker.afterFlush(() => {
+    if (typeof Utils !== 'undefined' && Utils.initializeUserSettings) {
+      Utils.initializeUserSettings();
+    }
+  });
 });

+ 53 - 0
client/components/boards/boardBody.css

@@ -231,6 +231,30 @@
       font-size: 1em !important; /* Keep original icon size */
     }
 
+/* Mobile iPhone: scale card details text and icons to 2x */
+body.mobile-mode.iphone-device .card-details {
+  font-size: 2em !important;
+}
+body.mobile-mode.iphone-device .card-details .fa,
+body.mobile-mode.iphone-device .card-details .icon,
+body.mobile-mode.iphone-device .card-details i,
+body.mobile-mode.iphone-device .card-details .emoji-icon,
+body.mobile-mode.iphone-device .card-details a,
+body.mobile-mode.iphone-device .card-details p,
+body.mobile-mode.iphone-device .card-details span,
+body.mobile-mode.iphone-device .card-details div,
+body.mobile-mode.iphone-device .card-details button,
+body.mobile-mode.iphone-device .card-details input,
+body.mobile-mode.iphone-device .card-details select,
+body.mobile-mode.iphone-device .card-details textarea {
+  font-size: inherit !important;
+}
+/* Section titles slightly larger than content but not as big as card title */
+body.mobile-mode.iphone-device .card-details .card-details-item-title {
+  font-size: 1.1em !important;
+  font-weight: bold;
+}
+
 /* Ensure scrollbars are positioned correctly */
 #content[style*="overflow-x: auto"]::-webkit-scrollbar:vertical {
   width: 12px;
@@ -263,6 +287,35 @@
   animation: fadeIn 0.2s;
   z-index: 16;
 }
+
+/* Fix for mobile Safari: ensure overlay stays behind card details */
+@media screen and (max-width: 800px) {
+  .board-wrapper .board-canvas .board-overlay {
+    z-index: 17 !important;
+  }
+  
+  /* In desktop mode on small screens, still keep overlay behind card */
+  body.desktop-mode .board-wrapper .board-canvas .board-overlay {
+    z-index: 17 !important;
+  }
+}
+
+/* In mobile mode, lower the overlay z-index to stay behind card details */
+body.mobile-mode .board-wrapper .board-canvas .board-overlay {
+  z-index: 17 !important;
+}
+
+/* iPhone in desktop mode: remove overlay to avoid blocking card */
+body.desktop-mode.iphone-device .board-wrapper .board-canvas .board-overlay {
+  display: none !important;
+  pointer-events: none !important;
+}
+
+/* Desktop mode: hide overlay to allow multiple cards and board interaction */
+body.desktop-mode .board-wrapper .board-canvas .board-overlay {
+  display: none !important;
+  pointer-events: none !important;
+}
 .board-wrapper .board-canvas.is-dragging-active .open-minicard-composer,
 .board-wrapper .board-canvas.is-dragging-active .minicard-wrapper.is-checked {
   display: none;

+ 4 - 0
client/components/boards/boardBody.jade

@@ -58,6 +58,10 @@ template(name="boardBody")
               +swimlane(this)
           else
             +listsGroup(currentBoard)
+      //- Render multiple open cards in desktop mode
+      unless isMiniScreen
+        each openCards
+          +cardDetails(this cardIndex=@index)
       +sidebar
 
 template(name="calendarView")

+ 19 - 0
client/components/boards/boardBody.js

@@ -516,6 +516,16 @@ BlazeComponent.extendComponent({
     return isMiniScreen && currentCardId;
   },
 
+  openCards() {
+    // In desktop mode, return array of all open cards
+    const isMobile = Utils.getMobileMode();
+    if (!isMobile) {
+      const openCardIds = Session.get('openCards') || [];
+      return openCardIds.map(id => ReactiveCache.getCard(id)).filter(card => card);
+    }
+    return [];
+  },
+
   goHome() {
     FlowRouter.go('home');
   },
@@ -1642,6 +1652,15 @@ BlazeComponent.extendComponent({
 
         // Open card the same way as clicking a minicard - set currentCard session
         // This shows the full card details overlay, not a popup
+        // In desktop mode, add to openCards array to support multiple cards
+        const isMobile = Utils.getMobileMode();
+        if (!isMobile) {
+          const openCards = Session.get('openCards') || [];
+          if (!openCards.includes(cardId)) {
+            openCards.push(cardId);
+            Session.set('openCards', openCards);
+          }
+        }
         Session.set('currentCard', cardId);
       });
     });

+ 22 - 6
client/components/boards/boardsList.css

@@ -583,9 +583,9 @@
 }
 
 .board-list .board-list-item .multi-selection-checkbox.is-checked {
-  background: #2196F3;
-  border-color: #2196F3;
-  box-shadow: 0 2px 8px rgba(33, 150, 243, 0.6);
+  background: #3cb500;
+  border-color: #3cb500;
+  box-shadow: 0 2px 8px rgba(60, 181, 0, 0.6);
   width: 24px !important;
   height: 24px !important;
   top: auto !important;
@@ -601,10 +601,22 @@
   font-weight: bold;
 }
 
+/* Grey checkboxes when grey icons setting is enabled */
+body.grey-icons-enabled .board-list .board-list-item .multi-selection-checkbox.is-checked {
+  background: #7a7a7a;
+  border-color: #7a7a7a;
+  box-shadow: 0 2px 8px rgba(122, 122, 122, 0.6);
+}
+
+body.grey-icons-enabled .board-list.is-multiselection-active .js-board.is-checked {
+  outline: 4px solid #7a7a7a;
+  box-shadow: 0 4px 12px rgba(122, 122, 122, 0.4);
+}
+
 .board-list.is-multiselection-active .js-board.is-checked {
-  outline: 4px solid #2196F3;
+  outline: 4px solid #3cb500;
   outline-offset: -4px;
-  box-shadow: 0 4px 12px rgba(33, 150, 243, 0.4);
+  box-shadow: 0 4px 12px rgba(60, 181, 0, 0.4);
 }
 
 /* Visual hint when multiselection is active */
@@ -645,7 +657,11 @@
 }
 .board-backgrounds-list .board-background-select .background-box i.fa-check {
   font-size: 25px;
-  color: #fff;
+  color: #3cb500;
+}
+/* Grey check icons when grey icons setting is enabled */
+body.grey-icons-enabled .board-backgrounds-list .board-background-select .background-box i.fa-check {
+  color: #7a7a7a;
 }
 
 /* Prevent Grey Icons from affecting checkmarks in background color list */

+ 2 - 3
client/components/cards/cardCustomFields.jade

@@ -55,10 +55,9 @@ template(name="cardCustomField-number")
 template(name="cardCustomField-checkbox")
   .js-checklist-item.checklist-item(class="{{#if data.value }}is-checked{{/if}}")
     if canModifyCard
-      .check-box-container
-        .check-box.materialCheckBox(class="{{#if data.value }}is-checked{{/if}}")
+      span.check-box-unicode {{#if data.value }}✅{{else}}⬜{{/if}}
     else
-      .materialCheckBox(class="{{#if data.value }}is-checked{{/if}}")
+      span.check-box-unicode {{#if data.value }}✅{{else}}⬜{{/if}}
 
 template(name="cardCustomField-currency")
     if canModifyCard

+ 1 - 0
client/components/cards/cardCustomFields.js

@@ -112,6 +112,7 @@ CardCustomField.register('cardCustomField');
   events() {
     return [
       {
+        'click .js-checklist-item .check-box-unicode': this.toggleItem,
         'click .js-checklist-item .check-box-container': this.toggleItem,
       },
     ];

+ 247 - 7
client/components/cards/cardDetails.css

@@ -118,6 +118,65 @@
   transition: flex-basis 0.1s;
   box-sizing: border-box;
 }
+
+/* Desktop mode: position card below board header */
+body.desktop-mode .card-details:not(.card-details-popup) {
+  position: fixed;
+  width: auto;
+  max-width: 800px;
+  flex-basis: auto;
+  border-radius: 8px;
+  z-index: 100;
+}
+
+/* Default position for first card or when dragged */
+body.desktop-mode .card-details:not(.card-details-popup):not([style*="left"]):not([style*="top"]) {
+  top: 50px;
+  left: 20px;
+  right: 20px;
+  bottom: 20px;
+}
+
+/* Stagger positions for multiple cards using nth-of-type */
+body.desktop-mode .card-details:not(.card-details-popup):nth-of-type(1) {
+  top: 50px;
+  left: 20px;
+}
+body.desktop-mode .card-details:not(.card-details-popup):nth-of-type(2) {
+  top: 80px;
+  left: 50px;
+}
+body.desktop-mode .card-details:not(.card-details-popup):nth-of-type(3) {
+  top: 110px;
+  left: 80px;
+}
+body.desktop-mode .card-details:not(.card-details-popup):nth-of-type(4) {
+  top: 140px;
+  left: 110px;
+}
+body.desktop-mode .card-details:not(.card-details-popup):nth-of-type(5) {
+  top: 170px;
+  left: 140px;
+}
+
+/* For expanded cards, set dimensions */
+body.desktop-mode .card-details:not(.card-details-popup):not(.card-details-collapsed) {
+  right: 20px;
+  bottom: 20px;
+}
+
+/* Collapsed card state - hide content and set height to title row only */
+.card-details.card-details-collapsed .card-details-canvas > *:not(.card-details-header) {
+  display: none;
+}
+.card-details.card-details-collapsed {
+  height: auto !important;
+  bottom: auto !important;
+  overflow: visible;
+}
+body.desktop-mode .card-details.card-details-collapsed {
+  bottom: auto !important;
+}
 .card-details .mCustomScrollBox {
   padding-left: 0;
 }
@@ -139,6 +198,49 @@
   display: inline-block;
   margin-right: 5px;
 }
+
+/* Collapse toggle triangle */
+.card-details .card-details-header .card-collapse-toggle {
+  float: left;
+  font-size: 20px;
+  padding: 7px 10px;
+  margin-left: -10px;
+  margin-right: 5px;
+  cursor: pointer;
+  user-select: none;
+  color: #000;
+}
+
+/* Bring to front / Send to back buttons */
+.card-details .card-details-header .card-bring-to-front,
+.card-details .card-details-header .card-send-to-back {
+  float: right;
+  font-size: 18px;
+  padding: 7px 8px;
+  margin-right: 5px;
+  cursor: pointer;
+  user-select: none;
+  color: #333;
+}
+
+.card-details .card-details-header .card-bring-to-front:hover,
+.card-details .card-details-header .card-send-to-back:hover {
+  color: #000;
+  background: rgba(0,0,0,0.05);
+  border-radius: 3px;
+}
+
+/* Drag handle */
+.card-details .card-details-header .card-drag-handle {
+  font-size: 20px;
+  padding: 8px 10px;
+  margin-right: 10px;
+  cursor: move;
+  user-select: none;
+  display: inline-block;
+  float: right;
+}
+
 .card-details .card-details-header .close-card-details,
 .card-details .card-details-header .maximize-card-details,
 .card-details .card-details-header .minimize-card-details,
@@ -156,11 +258,16 @@
   font-size: 24px;
   padding: 5px 10px 5px 10px;
   margin-right: -8px;
+  cursor: pointer;
+  user-select: none;
 }
-.card-details .card-details-header .close-card-details-mobile-web {
+.card-details .card-details-header .close-card-details-mobile-web,
+.card-details .card-details-header .card-mobile-desktop-toggle {
   font-size: 24px;
   padding: 5px;
-  margin-right: 40px;
+  margin-right: 5px;
+  cursor: pointer;
+  user-select: none;
 }
 .card-details .card-details-header .card-copy-button {
   font-size: 17px;
@@ -181,6 +288,36 @@
   padding: 10px;
   margin-right: 30px;
 }
+.card-details .card-details-header .card-mobile-desktop-toggle,
+.card-details .card-details-header .card-zoom-in,
+.card-details .card-details-header .card-zoom-out {
+  font-size: 24px;
+  padding: 5px 10px 5px 10px;
+  margin-right: 5px;
+  cursor: pointer;
+  user-select: none;
+  float: right;
+}
+
+/* Unify all card text to match title size */
+.card-details {
+  font-size: 1em;
+}
+.card-details p,
+.card-details span,
+.card-details div,
+.card-details a,
+.card-details label,
+.card-details input,
+.card-details textarea,
+.card-details select,
+.card-details button,
+.card-details .card-details-item-title,
+.card-details .card-label,
+.card-details .viewer {
+  font-size: inherit;
+  line-height: 1.4;
+}
 .card-details .card-details-header .card-details-watch {
   font-size: 17px;
   padding-left: 7px;
@@ -284,6 +421,19 @@
     position: fixed;
     resize: both;
   }
+  
+  /* Override for mobile mode even on larger screens */
+  body.mobile-mode .card-details {
+    width: 100vw !important;
+    top: 0 !important;
+    left: 0 !important;
+    right: 0 !important;
+    bottom: 0 !important;
+    height: 100vh !important;
+    max-height: 100vh !important;
+    resize: none !important;
+  }
+  
   .card-details-maximized {
     padding: 0;
     flex-shrink: 0;
@@ -335,19 +485,53 @@ input[type="submit"].attachment-add-link-submit {
 }
 @media screen and (max-width: 800px) {
   .card-details {
-    width: calc(100% - 1px);
-    padding: 0px 20px 0px 20px;
-    margin: 0px;
+    width: 100% !important;
+    padding: 0px 0px 0px 0px !important;
+    margin: 0px !important;
     transition: none;
-    overflow-y: revert;
-    overflow-x: revert;
+    overflow-y: auto;
+    overflow-x: hidden;
+    /* iOS Safari specific fixes */
+    -webkit-overflow-scrolling: touch;
+    position: fixed !important;
+    top: 0 !important;
+    left: 0 !important;
+    right: 0 !important;
+    bottom: 0 !important;
+    z-index: 100 !important;
+    height: 100vh !important;
+    max-height: 100vh !important;
+    border-radius: 0 !important;
+    box-shadow: none !important;
+  }
+  
+  /* Ensure card details are above everything on mobile */
+  body.mobile-mode .card-details {
+    z-index: 100 !important;
+    width: 100vw !important;
+    left: 0 !important;
+    right: 0 !important;
   }
   .card-details .card-details-canvas {
     width: 100%;
     padding-left: 0px;
+    padding: 0 15px;
   }
   .card-details .card-details-header .close-card-details {
     margin-right: 0px;
+    display: block !important;
+  }
+  .card-details .card-details-header .close-card-details-mobile-web {
+    display: block !important;
+    margin-right: 5px !important;
+  }
+  .card-details .card-details-header .card-mobile-desktop-toggle {
+    display: block !important;
+    margin-right: 5px !important;
+  }
+  .card-details .card-details-header .card-mobile-desktop-toggle {
+    display: block !important;
+    margin-right: 5px !important;
   }
   .card-details .card-details-header .card-details-menu {
     margin-right: 40px;
@@ -373,6 +557,62 @@ input[type="submit"].attachment-add-link-submit {
   .pop-over > .content-wrapper > .popup-container-depth-0 .card-details-header {
     margin: 0;
   }
+  /* iPhone mobile: enlarge header buttons and increase spacing */
+  body.mobile-mode.iphone-device .card-details .card-details-header {
+    padding-right: 16px;
+  }
+  body.mobile-mode.iphone-device .card-details .card-details-header .close-card-details,
+  body.mobile-mode.iphone-device .card-details .card-details-header .maximize-card-details,
+  body.mobile-mode.iphone-device .card-details .card-details-header .minimize-card-details,
+  body.mobile-mode.iphone-device .card-details .card-details-header .card-details-menu-mobile-web,
+  body.mobile-mode.iphone-device .card-details .card-details-header .card-copy-mobile-button,
+  body.mobile-mode.iphone-device .card-details .card-details-header .card-mobile-desktop-toggle,
+  body.mobile-mode.iphone-device .card-details .card-details-header .card-zoom-in,
+  body.mobile-mode.iphone-device .card-details .card-details-header .card-zoom-out {
+    font-size: 2em !important; /* 2x bigger */
+    padding: 0.3em !important;
+    margin-right: 0.75em !important; /* 2x space compared to default */
+    margin-left: 0 !important;
+  }
+  /* Avoid clipping of the close button on the right edge */
+  body.mobile-mode.iphone-device .card-details .card-details-header .close-card-details {
+    margin-right: 0.75em !important;
+  }
+  /* Enlarge the header title too */
+  body.mobile-mode.iphone-device .card-details .card-details-header .card-details-title {
+    font-size: 1.2em !important;
+    font-weight: bold;
+  }
+}
+
+/* Mobile mode styles - apply when body has mobile-mode class regardless of screen size */
+body.mobile-mode .card-details {
+  width: 100vw !important;
+  padding: 0px !important;
+  margin: 0px !important;
+  position: fixed !important;
+  top: 0 !important;
+  left: 0 !important;
+  right: 0 !important;
+  bottom: 0 !important;
+  z-index: 100 !important;
+  height: 100vh !important;
+  max-height: 100vh !important;
+  border-radius: 0 !important;
+  box-shadow: none !important;
+  overflow-y: auto !important;
+  overflow-x: hidden !important;
+  -webkit-overflow-scrolling: touch;
+}
+
+body.mobile-mode .card-details .card-details-canvas {
+  width: 100% !important;
+  padding: 0 15px !important;
+}
+
+body.mobile-mode .card-details .card-details-header .close-card-details,
+body.mobile-mode .card-details .card-details-header .close-card-details-mobile-web {
+  display: block !important;
 }
 .card-details-white {
   background: #fff !important;

+ 29 - 4
client/components/cards/cardDetails.jade

@@ -5,16 +5,25 @@ template(name="cardDetails")
 
   +attachmentViewer
 
-  section.card-details.js-card-details.nodragscroll(class='{{#if cardMaximized}}card-details-maximized{{/if}}' class='{{#if isPopup}}card-details-popup{{/if}}' class='{{#unless isVerticalScrollbars}}no-scrollbars{{/unless}}'): .card-details-canvas
+  section.card-details.js-card-details.nodragscroll(class='{{#if cardMaximized}}card-details-maximized{{/if}}' class='{{#if isPopup}}card-details-popup{{/if}}' class='{{#unless isVerticalScrollbars}}no-scrollbars{{/unless}}' class='{{#if cardCollapsed}}card-details-collapsed{{/if}}'): .card-details-canvas
     .card-details-header(class='{{#if colorClass}}card-details-{{colorClass}}{{/if}}')
       +inlinedForm(classNames="js-card-details-title")
         +editCardTitleForm
       else
         unless isMiniScreen
           unless isPopup
+            span.card-collapse-toggle.js-card-collapse-toggle(title="{{_ 'collapse-card'}}")
+              if cardCollapsed
+                | ▶
+              else
+                | 🔽
             a.close-card-details.js-close-card-details(title="{{_ 'close-card'}}")
               | ❌
             if canModifyCard
+              a.card-bring-to-front.js-card-bring-to-front(title="Bring to front")
+                | ⏫
+              a.card-send-to-back.js-card-send-to-back(title="Send to back")
+                | ⏬
               if cardMaximized
                 a.minimize-card-details.js-minimize-card-details(title="{{_ 'minimize-card'}}")
                   | 🔽
@@ -30,12 +39,28 @@ template(name="cardDetails")
               href="{{ originRelativeUrl }}"
             )
               span.emoji-icon 🔗
+            span.card-drag-handle.js-card-drag-handle(title="Drag card")
+              | ↕️
             span.copied-tooltip {{_ 'copied'}}
         else
-          unless isPopup
-            a.close-card-details.js-close-card-details(title="{{_ 'close-card'}}")
-              | ❌
+          a.close-card-details.js-close-card-details(title="{{_ 'close-card'}}")
+            | ❌
+          a.card-zoom-out.js-card-zoom-out(title="{{_ 'zoom-out'}}")
+            | 🔍➖
+          a.card-zoom-in.js-card-zoom-in(title="{{_ 'zoom-in'}}")
+            | 🔍➕
+          a.card-mobile-desktop-toggle.js-card-mobile-desktop-toggle(title="{{_ 'mobile-desktop-toggle'}}")
+            if mobileMode
+              | 🖥️
+            else
+              | 📱
           if canModifyCard
+            if cardMaximized
+              a.minimize-card-details.js-minimize-card-details(title="{{_ 'minimize-card'}}")
+                | 🔽
+            else
+              a.maximize-card-details.js-maximize-card-details(title="{{_ 'maximize-card'}}")
+                | 🔼
             a.card-details-menu-mobile-web.js-open-card-details-menu(title="{{_ 'cardDetailsActionsPopup-title'}}")
               | ☰
             a.card-copy-mobile-button.js-copy-link(

+ 121 - 2
client/components/cards/cardDetails.js

@@ -63,7 +63,11 @@ BlazeComponent.extendComponent({
       const boardBody = this.parentComponent().parentComponent();
       //in Miniview parent is Board, not BoardBody.
       if (boardBody !== null) {
-        boardBody.showOverlay.set(true);
+        // Only show overlay in mobile mode, not in desktop mode
+        const isMobile = Utils.getMobileMode();
+        if (isMobile) {
+          boardBody.showOverlay.set(true);
+        }
         boardBody.mouseHasEnterCardDetails = false;
       }
     }
@@ -93,6 +97,18 @@ BlazeComponent.extendComponent({
     return !Utils.getPopupCardId() && ReactiveCache.getCurrentUser().hasCardMaximized();
   },
 
+  cardCollapsed() {
+    const user = ReactiveCache.getCurrentUser();
+    if (user && user.profile) {
+      return !!user.profile.cardCollapsed;
+    }
+    if (Users.getPublicCardCollapsed) {
+      const stored = Users.getPublicCardCollapsed();
+      if (typeof stored === 'boolean') return stored;
+    }
+    return false;
+  },
+
   presentParentTask() {
     let result = this.currentBoard.presentParentTask;
     if (result === null || result === undefined) {
@@ -296,13 +312,88 @@ BlazeComponent.extendComponent({
     return [
       {
         ...events,
+        'click .js-card-collapse-toggle'() {
+          const user = ReactiveCache.getCurrentUser();
+          const currentState = user && user.profile ? !!user.profile.cardCollapsed : !!Users.getPublicCardCollapsed();
+          if (user) {
+            Meteor.call('setCardCollapsed', !currentState);
+          } else if (Users.setPublicCardCollapsed) {
+            Users.setPublicCardCollapsed(!currentState);
+          }
+        },
+        'click .js-card-bring-to-front'(event) {
+          event.preventDefault();
+          const $card = $(event.target).closest('.card-details');
+          // Find the highest z-index among all cards
+          let maxZ = 100;
+          $('.card-details').each(function() {
+            const z = parseInt($(this).css('z-index')) || 100;
+            if (z > maxZ) maxZ = z;
+          });
+          // Set this card's z-index to be higher
+          $card.css('z-index', maxZ + 1);
+        },
+        'click .js-card-send-to-back'(event) {
+          event.preventDefault();
+          const $card = $(event.target).closest('.card-details');
+          // Find the lowest z-index among all cards
+          let minZ = 100;
+          $('.card-details').each(function() {
+            const z = parseInt($(this).css('z-index')) || 100;
+            if (z < minZ) minZ = z;
+          });
+          // Set this card's z-index to be lower
+          $card.css('z-index', minZ - 1);
+        },
+        'mousedown .js-card-drag-handle'(event) {
+          event.preventDefault();
+          const $card = $(event.target).closest('.card-details');
+          const startX = event.clientX;
+          const startY = event.clientY;
+          const startLeft = $card.offset().left;
+          const startTop = $card.offset().top;
+          
+          const onMouseMove = (e) => {
+            const deltaX = e.clientX - startX;
+            const deltaY = e.clientY - startY;
+            $card.css({
+              left: startLeft + deltaX + 'px',
+              top: startTop + deltaY + 'px'
+            });
+          };
+          
+          const onMouseUp = () => {
+            $(document).off('mousemove', onMouseMove);
+            $(document).off('mouseup', onMouseUp);
+          };
+          
+          $(document).on('mousemove', onMouseMove);
+          $(document).on('mouseup', onMouseUp);
+        },
         'click .js-close-card-details'() {
           // Get board ID from either the card data or current board in session
           const card = this.currentData() || this.data();
           const boardId = (card && card.boardId) || Utils.getCurrentBoard()._id;
+          const cardId = card && card._id;
           
           if (boardId) {
-            // Clear the current card session to close the card
+            // In desktop mode, remove from openCards array
+            const isMobile = Utils.getMobileMode();
+            if (!isMobile && cardId) {
+              const openCards = Session.get('openCards') || [];
+              const filtered = openCards.filter(id => id !== cardId);
+              Session.set('openCards', filtered);
+              
+              // If this was the current card, clear it
+              if (Session.get('currentCard') === cardId) {
+                Session.set('currentCard', null);
+              }
+              
+              // Don't navigate away in desktop mode - just close the card
+              return;
+            }
+            
+            // Mobile mode: Clear the current card session to close the card
             Session.set('currentCard', null);
             
             // Navigate back to board without card
@@ -327,6 +418,34 @@ BlazeComponent.extendComponent({
           Meteor.call('changeDateFormat', dateFormat);
         },
         'click .js-open-card-details-menu': Popup.open('cardDetailsActions'),
+        // Mobile: switch to desktop popup view (maximize)
+        'click .js-mobile-switch-to-desktop'(event) {
+          event.preventDefault();
+          // Switch global mode to desktop so the card appears as desktop popup
+          Utils.setMobileMode(false);
+        },
+        'click .js-card-zoom-in'(event) {
+          event.preventDefault();
+          const current = Utils.getCardZoom();
+          const newZoom = Math.min(3.0, current + 0.1);
+          Utils.setCardZoom(newZoom);
+        },
+        'click .js-card-zoom-out'(event) {
+          event.preventDefault();
+          const current = Utils.getCardZoom();
+          const newZoom = Math.max(0.5, current - 0.1);
+          Utils.setCardZoom(newZoom);
+        },
+        'click .js-card-mobile-desktop-toggle'(event) {
+          event.preventDefault();
+          const currentMode = Utils.getMobileMode();
+          Utils.setMobileMode(!currentMode);
+        },
+        'click .js-card-mobile-desktop-toggle'(event) {
+          event.preventDefault();
+          const currentMode = Utils.getMobileMode();
+          Utils.setMobileMode(!currentMode);
+        },
         'submit .js-card-description'(event) {
           event.preventDefault();
           const description = this.currentComponent().getValue();

+ 51 - 1
client/components/cards/checklists.css

@@ -37,14 +37,23 @@ textarea.js-edit-checklist-item {
 .checklist-progress-bar-container .checklist-progress-bar {
   width: 80%;
   height: 10px;
+  background-color: #d6ebff !important;
+  border-radius: 16px;
 }
 .checklist-progress-bar-container .checklist-progress-bar .checklist-progress {
   color: #fff !important;
-  background-color: #2196f3 !important;
+  background-color: #3cb500 !important;
   padding: 0.01em 16px;
   border-radius: 16px;
   height: 100%;
 }
+/* Grey progress bar when grey icons setting is enabled */
+body.grey-icons-enabled .checklist-progress-bar-container .checklist-progress-bar {
+  background-color: #d9d9d9;
+}
+body.grey-icons-enabled .checklist-progress-bar-container .checklist-progress-bar .checklist-progress {
+  background-color: #7a7a7a !important;
+}
 .checklist-title {
   padding: 10px;
 }
@@ -105,6 +114,25 @@ textarea.js-edit-checklist-item {
   height: auto;
   overflow: hidden;
 }
+
+/* iPhone mobile: larger checklist titles and more spacing between items */
+body.mobile-mode.iphone-device .checklist-title .title {
+  font-size: 1.3em !important;
+  font-weight: bold;
+}
+
+body.mobile-mode.iphone-device .checklist-item {
+  margin-top: 12px !important;
+  margin-bottom: 8px !important;
+  padding: 8px 4px !important;
+  min-height: 44px; /* iOS recommended touch target size */
+}
+
+body.mobile-mode.iphone-device .checklist-item span.checklistitem-handle {
+  font-size: 1.5em !important;
+  padding-right: 15px !important;
+  width: 1.5em !important;
+}
 .checklist-item.is-checked.invisible {
   opacity: 0;
   height: 0;
@@ -134,6 +162,27 @@ textarea.js-edit-checklist-item {
   border-bottom: 2px solid #3cb500;
   border-right: 2px solid #3cb500;
 }
+/* Unicode checkbox icons styling */
+.checklist-item .check-box-unicode,
+.cardCustomField-checkbox .check-box-unicode {
+  font-size: 1.3em;
+  margin-right: 8px;
+  cursor: pointer;
+  display: inline-block;
+  vertical-align: middle;
+  line-height: 1;
+}
+/* Grey checkmarks when grey icons setting is enabled */
+body.grey-icons-enabled .checklist-item .check-box.is-checked {
+  border-bottom: 2px solid #7a7a7a;
+  border-right: 2px solid #7a7a7a;
+}
+body.grey-icons-enabled .checklist-item .check-box-unicode,
+body.grey-icons-enabled .cardCustomField-checkbox .check-box-unicode {
+  filter: grayscale(100%);
+  -webkit-filter: grayscale(100%);
+  opacity: 0.85;
+}
 .checklist-item .item-title {
   flex: 1;
 }
@@ -155,6 +204,7 @@ textarea.js-edit-checklist-item {
   width: 1.2em;
   text-align: center;
   color: #999;
+  cursor: pointer;
 }
 .js-delete-checklist-item,
 .js-convert-checklist-item-to-card {

+ 2 - 3
client/components/cards/checklists.jade

@@ -125,14 +125,13 @@ template(name='checklistItemDetail')
   .js-checklist-item.checklist-item(class="{{#if item.isFinished }}is-checked{{#if checklist.hideCheckedChecklistItems}} invisible{{/if}}{{/if}}{{#if checklist.hideAllChecklistItems}} is-checked invisible{{/if}}"
     role="checkbox" aria-checked="{{#if item.isFinished }}true{{else}}false{{/if}}" tabindex="0")
     if canModifyCard
-      .check-box-container
-        .check-box.materialCheckBox(class="{{#if item.isFinished }}is-checked{{/if}}")
+      span.check-box-unicode {{#if item.isFinished }}✅{{else}}⬜{{/if}}
       span.checklistitem-handle(title="{{_ 'dragChecklistItem'}}") ↕️
       .item-title.js-open-inlined-form.is-editable(class="{{#if item.isFinished }}is-checked{{/if}}")
         +viewer
           = item.title
     else
-      .materialCheckBox(class="{{#if item.isFinished }}is-checked{{/if}}")
+      span.check-box-unicode {{#if item.isFinished }}✅{{else}}⬜{{/if}}
       .item-title(class="{{#if item.isFinished }}is-checked{{/if}}")
         +viewer
           = item.title

+ 2 - 1
client/components/cards/checklists.js

@@ -65,7 +65,7 @@ BlazeComponent.extendComponent({
         $(self.itemsDom).sortable('option', 'disabled', !userIsMember());
         if (Utils.isTouchScreenOrShowDesktopDragHandles()) {
           $(self.itemsDom).sortable({
-            handle: 'span.fa.checklistitem-handle',
+            handle: 'span.checklistitem-handle',
           });
         }
       }
@@ -360,6 +360,7 @@ BlazeComponent.extendComponent({
   events() {
     return [
       {
+        'click .js-checklist-item .check-box-unicode': this.toggleItem,
         'click .js-checklist-item .check-box-container': this.toggleItem,
       },
     ];

+ 28 - 0
client/components/cards/subtasks.css

@@ -87,6 +87,15 @@ textarea.js-edit-subtask-item {
   top: 0;
   bottom: -600px;
   right: 0;
+  z-index: 15;
+}
+
+/* Fix for mobile Safari: ensure this doesn't block card interaction */
+@media screen and (max-width: 800px) {
+  #card-details-overlay {
+    z-index: 15;
+    pointer-events: none;
+  }
 }
 .subtasks {
   background: #f7f7f7;
@@ -127,6 +136,25 @@ textarea.js-edit-subtask-item {
   border-bottom: 2px solid #3cb500;
   border-right: 2px solid #3cb500;
 }
+/* Unicode checkbox icons styling */
+.subtasks-item .check-box-unicode {
+  font-size: 1.3em;
+  margin-right: 8px;
+  cursor: pointer;
+  display: inline-block;
+  vertical-align: middle;
+  line-height: 1;
+}
+/* Grey checkmarks when grey icons setting is enabled */
+body.grey-icons-enabled .subtasks-item .check-box.is-checked {
+  border-bottom: 2px solid #7a7a7a;
+  border-right: 2px solid #7a7a7a;
+}
+body.grey-icons-enabled .subtasks-item .check-box-unicode {
+  filter: grayscale(100%);
+  -webkit-filter: grayscale(100%);
+  opacity: 0.85;
+}
 .subtasks-item .item-title {
   flex: 1;
   padding-left: 10px;

+ 2 - 2
client/components/cards/subtasks.jade

@@ -74,12 +74,12 @@ template(name="subtasksItems")
 template(name='subtaskItemDetail')
   .js-subtasks-item.subtasks-item
     if canModifyCard
-      .check-box.materialCheckBox(class="{{#if item.isFinished }}is-checked{{/if}}")
+      span.check-box-unicode {{#if item.isFinished }}✅{{else}}⬜{{/if}}
       .item-title.js-open-inlined-form.is-editable(class="{{#if item.isFinished }}is-checked{{/if}}")
         +viewer
           = item.title
     else
-      .materialCheckBox(class="{{#if item.isFinished }}is-checked{{/if}}")
+      span.check-box-unicode {{#if item.isFinished }}✅{{else}}⬜{{/if}}
       .item-title(class="{{#if item.isFinished }}is-checked{{/if}}")
         +viewer
           = item.title

+ 13 - 1
client/components/cards/subtasks.js

@@ -104,7 +104,19 @@ BlazeComponent.extendComponent({
 }).register('subtasks');
 
 BlazeComponent.extendComponent({
-  // ...
+  toggleItem() {
+    const item = this.currentData().item;
+    if (item && item._id) {
+      item.toggleItem();
+    }
+  },
+  events() {
+    return [
+      {
+        'click .js-subtasks-item .check-box-unicode': this.toggleItem,
+      },
+    ];
+  },
 }).register('subtaskItemDetail');
 
 BlazeComponent.extendComponent({

+ 7 - 0
client/components/forms/forms.css

@@ -315,11 +315,18 @@ textarea::-moz-placeholder {
   margin-right: 6px;
   border-top: 2px solid transparent;
   border-left: 2px solid transparent;
+  border-bottom: 2px solid #3cb500;
+  border-right: 2px solid #3cb500;
   transform: rotate(40deg);
   -webkit-backface-visibility: hidden;
   backface-visibility: hidden;
   transform-origin: 100% 100%;
 }
+/* Grey checkmarks when grey icons setting is enabled */
+body.grey-icons-enabled .materialCheckBox.is-checked {
+  border-bottom: 2px solid #7a7a7a;
+  border-right: 2px solid #7a7a7a;
+}
 .button-link {
   background: #fff;
   background: linear-gradient(#fff, #f5f5f5);

+ 76 - 31
client/components/lists/list.css

@@ -282,7 +282,7 @@ body.list-resizing-active * {
   margin: 0 auto;
 }
 .list.list-collapsed .list-header .js-collapse {
-  margin: 0 auto 20px auto;
+  margin: 0 auto 0 auto;
   z-index: 10;
   padding: 8px 12px;
   font-size: 12px;
@@ -290,6 +290,12 @@ body.list-resizing-active * {
   display: block;
   width: fit-content;
 }
+.list.list-collapsed .list-header .list-header-handle {
+  position: absolute !important;
+  top: 30px !important;
+  right: 1.5vw !important;
+  z-index: 15 !important;
+}
 .list.list-collapsed .list-header .list-rotated {
   width: auto !important;
   height: auto !important;
@@ -297,7 +303,6 @@ body.list-resizing-active * {
   position: relative !important;
   overflow: visible !important;
 }
-
 .list.list-collapsed .list-header .list-rotated h2.list-header-name {
   text-align: left;
   overflow: visible;
@@ -308,15 +313,15 @@ body.list-resizing-active * {
   color: #333;
   background-color: rgba(255, 255, 255, 0.95);
   border: 1px solid #ddd;
-  padding: 8px 4px;
+  padding: 0;
   border-radius: 4px;
-  margin: 0 auto;
-  width: 25vh;
-  height: 60vh;
+  margin: 0;
+  width: 100vh;
+  height: 30px;
   position: absolute;
-  left: 50%;
+  left: 40px;
   top: 50%;
-  transform: translate(calc(-50% + 50px), -50%) rotate(0deg);
+  transform: translateY(calc(-50% + 20px)) rotate(0deg);
   z-index: 10;
   visibility: visible !important;
   opacity: 1 !important;
@@ -415,22 +420,42 @@ body.list-resizing-active * {
   color: #a6a6a6;
   margin-right: 15px;
 }
+/* List header collapse button styling */
+.list-header .list-header-collapse-container {
+  display: flex;
+  flex-direction: row;
+  align-items: flex-start;
+  gap: 10px;
+  flex: 1;
+  min-width: 0;
+}
+
 .list-header .js-collapse {
   color: #a6a6a6;
-  margin-right: 15px;
   display: inline-block;
   vertical-align: middle;
   padding: 5px 8px;
-  border: 1px solid #ccc;
-  border-radius: 4px;
-  background-color: #f5f5f5;
+  border: none;
+  border-radius: 0;
+  background-color: transparent;
   cursor: pointer;
-  font-size: 14px;
+  font-size: 18px;
+  line-height: 1;
+  min-width: 30px;
+  text-align: center;
+  flex-shrink: 0;
+  text-decoration: none;
+  margin: 0;
 }
 .list-header .js-collapse:hover {
-  background-color: #e0e0e0;
+  background-color: transparent;
   color: #333;
 }
+
+.list-header .list-header-collapse-container > div {
+  flex: 1;
+  min-width: 0;
+}
 .list.list-collapsed .list-header .js-collapse {
   display: inline-block !important;
   visibility: visible !important;
@@ -459,17 +484,18 @@ body.list-resizing-active * {
     position: relative !important;
   }
   .list.list-collapsed .list-header .list-rotated h2.list-header-name {
-    width: 15vh;
+    width: 100vh;
     font-size: 12px;
     height: 30px;
     line-height: 1.2;
-    padding: 8px 4px;
-    margin: 0 auto;
+    padding: 0;
+    margin: 0;
+    overflow: visible;
     position: absolute;
-    left: 50%;
+    left: 40px;
     top: 50%;
-    transform: translate(calc(-50% + 50px), -50%) rotate(0deg);
-    text-align: left;
+    transform: translateY(calc(-50% + 120px)) rotate(0deg);
+    text-align: center;
     visibility: visible !important;
     opacity: 1 !important;
     display: block !important;
@@ -499,17 +525,18 @@ body.list-resizing-active * {
     position: relative !important;
   }
   .list.list-collapsed .list-header .list-rotated h2.list-header-name {
-    width: 15vh;
+    width: 100vh;
     font-size: 12px;
     height: 30px;
     line-height: 1.2;
-    padding: 8px 4px;
-    margin: 0 auto;
+    padding: 0;
+    margin: 0;
+    overflow: visible;
     position: absolute;
-    left: 50%;
+    left: 40px;
     top: 50%;
-    transform: translate(calc(-50% + 50px), -50%) rotate(0deg);
-    text-align: left;
+    transform: translateY(calc(-50% + 120px)) rotate(0deg);
+    text-align: center;
     visibility: visible !important;
     opacity: 1 !important;
     display: block !important;
@@ -539,16 +566,17 @@ body.list-resizing-active * {
     position: relative !important;
   }
   .list.list-collapsed .list-header .list-rotated h2.list-header-name {
-    width: 15vh;
+    width: 100vh;
     font-size: 12px;
     height: 30px;
     line-height: 1.2;
-    padding: 8px 4px;
-    margin: 0 auto;
+    padding: 0;
+    margin: 0;
+    overflow: visible;
     position: absolute;
-    left: 50%;
+    left: 40px;
     top: 50%;
-    transform: translate(calc(-50% + 50px), -50%) rotate(0deg);
+    transform: translateY(calc(-50% + 40px)) rotate(0deg);
     text-align: left;
     visibility: visible !important;
     opacity: 1 !important;
@@ -1053,6 +1081,23 @@ body.list-resizing-active * {
   grid-row: 1/3 !important;
   grid-column: 1 !important;
 }
+
+/* Allow long list titles to expand on desktop (non-mobile, non-collapsed) */
+.list:not(.mobile-view):not(.list-collapsed) .list-header {
+  overflow: visible !important;
+}
+
+.list:not(.mobile-view):not(.list-collapsed) .list-header .list-header-name {
+  /* Permit wrapping and full visibility */
+  white-space: normal !important;
+  overflow: visible !important;
+  text-overflow: clip !important;
+  display: inline-block !important;
+  /* Reserve space for right-side controls (menu, handle, count) */
+  max-width: calc(100% - 120px) !important;
+  /* Break long words to avoid overflow */
+  word-break: break-word !important;
+}
 .link-board-wrapper {
   display: flex;
   align-items: baseline;

+ 3 - 2
client/components/lists/list.jade

@@ -3,8 +3,9 @@ template(name='list')
                 style="{{#unless collapsed}}min-width:{{listWidth}}px;max-width:{{listConstraint}}px;{{/unless}}"
                 class="{{#if collapsed}}list-collapsed{{/if}} {{#if autoWidth}}list-auto-width{{/if}} {{#if isMiniScreen}}mobile-view{{/if}}")
     +listHeader
-    +listBody
-    .list-resize-handle.js-list-resize-handle.nodragscroll
+    unless collapsed
+      +listBody
+      .list-resize-handle.js-list-resize-handle.nodragscroll
 
 template(name='miniList')
   a.mini-list.js-select-list.js-list(id="js-list-{{_id}}" class="{{#if isMiniScreen}}mobile-view{{/if}}")

+ 11 - 3
client/components/lists/list.js

@@ -279,7 +279,8 @@ BlazeComponent.extendComponent({
     
     // Only enable resize for non-collapsed, non-auto-width lists
     const isAutoWidth = this.autoWidth();
-    if (list.collapsed || isAutoWidth) {
+    const isCollapsed = Utils.getListCollapseState(list);
+    if (isCollapsed || isAutoWidth) {
       $resizeHandle.hide();
       return;
     }
@@ -433,9 +434,10 @@ BlazeComponent.extendComponent({
     });
     
     
-    // Reactively update resize handle visibility when auto-width changes
+    // Reactively update resize handle visibility when auto-width or collapse changes
     component.autorun(() => {
-      if (component.autoWidth()) {
+      const collapsed = Utils.getListCollapseState(list);
+      if (component.autoWidth() || collapsed) {
         $resizeHandle.hide();
       } else {
         $resizeHandle.show();
@@ -452,6 +454,12 @@ BlazeComponent.extendComponent({
   },
 }).register('list');
 
+Template.list.helpers({
+  collapsed() {
+    return Utils.getListCollapseState(this);
+  },
+});
+
 Template.miniList.events({
   'click .js-select-list'() {
     const listId = this._id;

+ 21 - 21
client/components/lists/listHeader.jade

@@ -30,20 +30,22 @@ template(name="listHeader")
             | &nbsp;
             span.list-sum-badge(title="{{_ 'sum-of-number-fields'}}") ∑ {{numberFieldsSum}}
       else
-        if collapsed
-          a.js-collapse(title="{{_ 'uncollapse'}}")
-            | ⬅️
-            | ➡️
-        div(class="{{#if collapsed}}list-rotated{{/if}}")
-          h2.list-header-name(
-            title="{{ moment modifiedAt 'LLL' }}"
-            class="{{#if currentUser.isBoardMember}}{{#unless currentUser.isCommentOnly}}{{#unless currentUser.isWorker}}js-open-inlined-form is-editable{{/unless}}{{/unless}}{{/if}}")
-            +viewer
-              = title
-            if wipLimit.enabled
-             |&nbsp;(
-             span(class="{{#if exceededWipLimit}}highlight{{/if}}") {{cards.length}}
-             |/#{wipLimit.value})
+        div.list-header-collapse-container
+          a.list-collapse-indicator.js-collapse(title="{{_ 'collapse'}}")
+            if collapsed
+              | ▶
+            else
+              | 🔽
+          div(class="{{#if collapsed}}list-rotated{{/if}}")
+            h2.list-header-name(
+              title="{{ moment modifiedAt 'LLL' }}"
+              class="{{#if currentUser.isBoardMember}}{{#unless currentUser.isCommentOnly}}{{#unless currentUser.isWorker}}js-open-inlined-form is-editable{{/unless}}{{/unless}}{{/if}}")
+              +viewer
+                = title
+              if wipLimit.enabled
+               |&nbsp;(
+               span(class="{{#if exceededWipLimit}}highlight{{/if}}") {{cards.length}}
+               |/#{wipLimit.value})
         unless collapsed
           if showCardsCountForList cards.length
             span.cardCount {{cardsCount}} {{cardsCountForListIsOne cards.length}}
@@ -64,6 +66,10 @@ template(name="listHeader")
           unless currentUser.isWorker
             a.list-header-handle.handle.js-list-handle ↕️
       else if currentUser.isBoardMember
+        if currentUser.isBoardMember
+          unless currentUser.isCommentOnly
+            unless currentUser.isWorker
+              a.list-header-handle.handle.js-list-handle ↕️
         if isWatching
           i.list-header-watch-icon | 👁️
         unless collapsed
@@ -73,14 +79,8 @@ template(name="listHeader")
               //  a.fa.js-list-star.list-header-plus-top(class="fa-star{{#unless starred}}-o{{/unless}}")
               if canSeeAddCard
                 a.js-add-card.list-header-plus-top(title="{{_ 'add-card-to-top-of-list'}}") ➕
-                a.js-collapse(title="{{_ 'collapse'}}")
-                  | ⬅️
-                  | ➡️
+
                 a.js-open-list-menu(title="{{_ 'listActionPopup-title'}}") ☰
-            if currentUser.isBoardMember
-              unless currentUser.isCommentOnly
-                unless currentUser.isWorker
-                  a.list-header-handle.handle.js-list-handle ↕️
 
 template(name="editListTitleForm")
   .list-composer

+ 4 - 3
client/components/lists/listHeader.js

@@ -34,13 +34,14 @@ BlazeComponent.extendComponent({
   },
   collapsed(check = undefined) {
     const list = Template.currentData();
-    const status = list.isCollapsed();
+    const status = Utils.getListCollapseState(list);
     if (check === undefined) {
       // just check
       return status;
     } else {
-      list.collapse(!status);
-      return !status;
+      const next = typeof check === 'boolean' ? check : !status;
+      Utils.setListCollapseState(list, next);
+      return next;
     }
   },
   editTitle(event) {

+ 11 - 5
client/components/main/header.css

@@ -339,15 +339,20 @@
       width: 100%;
       min-width: 3vw;
       font-size: clamp(12px, 2vw, 14px);
+      box-sizing: border-box;
+      -webkit-appearance: none;
+      appearance: none;
+      flex: 0 0 auto;
     }
 
     /* Make zoom input wider on all mobile screens */
     @media screen and (max-width: 800px),
        screen and (max-device-width: 932px) and (-webkit-min-device-pixel-ratio: 3) {
       #header-quick-access .zoom-controls .zoom-input {
-        min-width: 50px !important; /* Wider on mobile */
-        width: 50px !important; /* Fixed width to show all numbers */
-        font-size: 14px !important; /* Slightly larger text */
+        min-width: 80px !important; /* Wider on mobile to show 3 digits */
+        width: 80px !important; /* Fixed width to show 100 fully */
+        font-size: 16px !important; /* Slightly larger text */
+        flex: 0 0 80px !important; /* Prevent shrinking in flex */
       }
     }
 
@@ -850,8 +855,9 @@
     #header-quick-access .zoom-controls .zoom-input {
       font-size: 16px !important; /* Larger input text */
       padding: 0.5vh 0.8vw !important;
-      min-width: 6vw !important; /* Much wider for mobile */
-      width: 60px !important; /* Fixed width to show all numbers */
+      min-width: 80px !important; /* Wider to fit 100 */
+      width: 80px !important; /* Fixed width to show 100 fully */
+      flex: 0 0 80px !important; /* Prevent shrinking in flex */
     }
 
     /* Make mobile mode toggle larger */

+ 55 - 0
client/components/main/layouts.css

@@ -81,6 +81,27 @@ body {
   display: flex;
   flex-direction: column;
   height: 100vh;
+  /* iOS Safari fixes */
+  -webkit-overflow-scrolling: touch;
+}
+
+/* Mobile mode specific fixes for iOS Safari */
+body.mobile-mode {
+  overflow-x: hidden;
+  position: fixed;
+  width: 100%;
+  height: 100vh;
+  /* Prevent iOS Safari bounce scroll */
+  overscroll-behavior: none;
+  -webkit-overflow-scrolling: touch;
+}
+
+/* Ensure content area is scrollable in mobile mode */
+body.mobile-mode #content {
+  overflow-y: auto;
+  overflow-x: hidden;
+  -webkit-overflow-scrolling: touch;
+  height: calc(100vh - 48px);
 }
 #content {
   position: relative;
@@ -899,6 +920,40 @@ a:not(.disabled).is-active i.fa {
     height: 100%;
   }
 }
+
+/* iOS Safari Mobile Mode Fixes */
+@media screen and (max-width: 800px) {
+  /* Prevent scrolling issues on iOS Safari when card popup is open */
+  body.mobile-mode {
+    overflow: hidden;
+    position: fixed;
+    width: 100%;
+    height: 100vh;
+  }
+  
+  /* Fix z-index stacking for mobile Safari */
+  body.mobile-mode .board-wrapper {
+    z-index: 1;
+  }
+  
+  body.mobile-mode .board-wrapper .board-canvas .board-overlay {
+    z-index: 17 !important;
+  }
+  
+  body.mobile-mode .card-details {
+    z-index: 100 !important;
+  }
+  
+  body.mobile-mode .pop-over {
+    z-index: 999;
+  }
+  
+  /* Ensure smooth scrolling on iOS */
+  body.mobile-mode .card-details,
+  body.mobile-mode .pop-over .content-wrapper {
+    -webkit-overflow-scrolling: touch;
+  }
+}
 @-moz-keyframes lds-roller {
   0% {
     transform: rotate(0deg);

+ 3 - 1
client/components/main/layouts.jade

@@ -2,8 +2,10 @@ template(name="main")
   html(lang="{{TAPi18n.getLanguage}}")
     head
       title
-      meta(name="viewport" content="width=device-width, initial-scale=1, maximum-scale=5, user-scalable=yes")
+      meta(name="viewport" content="width=device-width, initial-scale=1, maximum-scale=5, user-scalable=yes, viewport-fit=cover")
       meta(http-equiv="X-UA-Compatible" content="IE=edge")
+      meta(name="apple-mobile-web-app-capable" content="yes")
+      meta(name="apple-mobile-web-app-status-bar-style" content="black-translucent")
       //- XXX We should use pathFor in the following `href` to support the case
         where the application is deployed with a path prefix, but it seems to be
         difficult to do that cleanly with Blaze -- at least without adding extra

+ 29 - 0
client/components/main/popup.css

@@ -538,6 +538,7 @@
   position: absolute;
   top: 6px;
   right: 12px;
+  color: #3cb500;
 }
 .pop-over-list .pop-over-list.checkable li.active a {
   padding-right: 28px;
@@ -545,6 +546,10 @@
 .pop-over-list .pop-over-list.checkable li.active a .fa-check {
   display: block;
 }
+/* Grey check icons when grey icons setting is enabled */
+body.grey-icons-enabled .pop-over-list .pop-over-list.checkable .fa-check {
+  color: #7a7a7a;
+}
 .pop-over.miniprofile .header {
   border-bottom-color: transparent;
   height: 30px;
@@ -590,6 +595,10 @@
     overflow: hidden;
     margin-top: 0px;
     border: 0px solid #dbdbdb;
+    /* Ensure popups appear above card details on mobile */
+    z-index: 999999 !important;
+    /* iOS Safari scrolling fix */
+    -webkit-overflow-scrolling: touch;
   }
   .pop-over .header {
     color: #fff;
@@ -674,3 +683,23 @@
     transform: none !important;
   }
 }
+
+/* Force full-screen popups in mobile mode regardless of screen width */
+body.mobile-mode .pop-over {
+  position: fixed !important;
+  top: 0 !important;
+  left: 0 !important;
+  right: 0 !important;
+  bottom: 0 !important;
+  width: 100vw !important;
+  height: 100vh !important;
+  max-width: 100vw !important;
+  max-height: 100vh !important;
+}
+body.mobile-mode .pop-over .content-wrapper {
+  width: 100% !important;
+  height: calc(100vh - 48px) !important;
+  max-height: calc(100vh - 48px) !important;
+  overflow-y: auto !important;
+  overflow-x: hidden !important;
+}

+ 7 - 2
client/components/settings/settingBody.css

@@ -137,8 +137,13 @@
   padding: 0.5rem 0.5rem;
 }
 .setting-content .content-body .main-body ul li a .is-checked {
-  border-bottom: 2px solid #2980b9;
-  border-right: 2px solid #2980b9;
+  border-bottom: 2px solid #3cb500;
+  border-right: 2px solid #3cb500;
+}
+/* Grey checkmarks when grey icons setting is enabled */
+body.grey-icons-enabled .setting-content .content-body .main-body ul li a .is-checked {
+  border-bottom: 2px solid #7a7a7a;
+  border-right: 2px solid #7a7a7a;
 }
 .setting-content .content-body .main-body ul li a span {
   padding: 0 0.5rem;

+ 13 - 0
client/components/sidebar/sidebar.css

@@ -68,6 +68,14 @@
   transform-origin: 100% 100% !important;
 }
 
+/* Grey checkmarks when grey icons setting is enabled */
+body.grey-icons-enabled .sidebar .materialCheckBox.is-checked,
+body.grey-icons-enabled .boardCardSettingsPopup .materialCheckBox.is-checked,
+body.grey-icons-enabled .boardSubtaskSettingsPopup .materialCheckBox.is-checked {
+  border-bottom: 2px solid #7a7a7a !important;
+  border-right: 2px solid #7a7a7a !important;
+}
+
 /* Card Settings 3-column grid layout */
 .card-settings-grid {
   display: grid;
@@ -130,6 +138,11 @@
 }
 .sidebar .sidebar-content ul.sidebar-list li > a .fa.fa-check {
   margin: 0 4px;
+  color: #3cb500;
+}
+/* Grey check icons when grey icons setting is enabled */
+body.grey-icons-enabled .sidebar .sidebar-content ul.sidebar-list li > a .fa.fa-check {
+  color: #7a7a7a;
 }
 .sidebar .sidebar-content ul.sidebar-list li .minicard {
   padding: 6px 8px 4px;

+ 5 - 0
client/components/swimlanes/swimlaneHeader.jade

@@ -26,6 +26,11 @@ template(name="swimlaneFixedHeader")
     if currentUser
       unless currentUser.isCommentOnly
         unless currentUser.isWorker
+          a.swimlane-collapse-indicator.js-collapse-swimlane.swimlane-header-collapse(title="{{_ 'collapse'}}")
+            if collapseSwimlane
+              | ▶
+            else
+              | 🔽
           a.js-open-add-swimlane-menu.swimlane-header-plus-icon(title="{{_ 'add-swimlane'}}")
             | ➕
           unless isTouchScreen

+ 8 - 3
client/components/swimlanes/swimlaneHeader.js

@@ -20,13 +20,14 @@ BlazeComponent.extendComponent({
   },
   collapsed(check = undefined) {
     const swimlane = Template.currentData();
-    const status = swimlane.isCollapsed();
+    const status = Utils.getSwimlaneCollapseState(swimlane);
     if (check === undefined) {
       // just check
       return status;
     } else {
-      swimlane.collapse(!status);
-      return !status;
+      const next = typeof check === 'boolean' ? check : !status;
+      Utils.setSwimlaneCollapseState(swimlane, next);
+      return next;
     }
   },
 
@@ -49,6 +50,10 @@ Template.swimlaneFixedHeader.helpers({
   isBoardAdmin() {
     return ReactiveCache.getCurrentUser().isBoardAdmin();
   },
+  collapseSwimlane() {
+    const swimlane = Template.currentData();
+    return Utils.getSwimlaneCollapseState(swimlane);
+  },
   isTitleDefault(title) {
     // https://github.com/wekan/wekan/issues/4763
     // https://github.com/wekan/wekan/issues/4742

+ 23 - 0
client/components/swimlanes/swimlanes.css

@@ -130,6 +130,29 @@
   pointer-events: auto;
 }
 
+/* Swimlane collapse button styling - matches list collapse button */
+.swimlane .swimlane-header-wrap .swimlane-header-menu .swimlane-collapse-indicator {
+  color: #a6a6a6;
+  display: inline-block;
+  vertical-align: middle;
+  padding: 5px 8px;
+  border: none;
+  border-radius: 0;
+  background-color: transparent;
+  cursor: pointer;
+  font-size: 18px;
+  line-height: 1;
+  min-width: 30px;
+  text-align: center;
+  text-decoration: none;
+  margin: 0;
+  flex-shrink: 0;
+}
+.swimlane .swimlane-header-wrap .swimlane-header-menu .swimlane-collapse-indicator:hover {
+  background-color: transparent;
+  color: #333;
+}
+
 #js-swimlane-height-edit .swimlane-height-error {
   display: none;
 }

+ 49 - 4
client/components/swimlanes/swimlanes.js

@@ -283,6 +283,9 @@ BlazeComponent.extendComponent({
     
     // Wait for DOM to be ready
     setTimeout(() => {
+      const handleSelector = Utils.isTouchScreenOrShowDesktopDragHandles()
+        ? '.js-list-handle'
+        : '.js-list-header';
       const $lists = this.$('.js-list');
       
       const $parent = $lists.parent();
@@ -306,7 +309,7 @@ BlazeComponent.extendComponent({
           items: '.js-list:not(.js-list-composer)',
           placeholder: 'list placeholder',
           distance: 7,
-          handle: '.js-list-handle',
+          handle: handleSelector,
           disabled: !Utils.canModifyBoard(),
           start(evt, ui) {
             ui.helper.css('z-index', 1000);
@@ -319,6 +322,15 @@ BlazeComponent.extendComponent({
             boardComponent.setIsDragging(false);
           }
         });
+        // Reactively update handle when user toggles desktop drag handles
+        this.autorun(() => {
+          const newHandle = Utils.isTouchScreenOrShowDesktopDragHandles()
+            ? '.js-list-handle'
+            : '.js-list-header';
+          if ($parent.data('uiSortable') || $parent.data('sortable')) {
+            try { $parent.sortable('option', 'handle', newHandle); } catch (e) {}
+          }
+        });
       } else {
       }
     }, 100);
@@ -684,6 +696,10 @@ Template.swimlane.helpers({
   lists() {
     // Return per-swimlane lists for this swimlane
     return this.myLists();
+  },
+
+  collapseSwimlane() {
+    return Utils.getSwimlaneCollapseState(this);
   }
 });
 
@@ -691,6 +707,9 @@ Template.swimlane.helpers({
 setTimeout(() => {
   const $swimlaneElements = $('.swimlane');
   const $listsGroupElements = $('.list-group');
+  const computeHandle = () => (
+    Utils.isTouchScreenOrShowDesktopDragHandles() ? '.js-list-handle' : '.js-list-header'
+  );
   
   // Initialize sortable on ALL swimlane elements (even empty ones)
   $swimlaneElements.each(function(index) {
@@ -707,7 +726,7 @@ setTimeout(() => {
         items: '.js-list:not(.js-list-composer)',
         placeholder: 'list placeholder',
         distance: 7,
-        handle: '.js-list-handle',
+        handle: computeHandle(),
         disabled: !Utils.canModifyBoard(),
         start(evt, ui) {
           ui.helper.css('z-index', 1000);
@@ -831,6 +850,13 @@ setTimeout(() => {
           });
         }
       });
+      // Reactively adjust handle when setting changes
+      Tracker.autorun(() => {
+        const newHandle = computeHandle();
+        if ($swimlane.data('uiSortable') || $swimlane.data('sortable')) {
+          try { $swimlane.sortable('option', 'handle', newHandle); } catch (e) {}
+        }
+      });
     }
   });
   
@@ -849,7 +875,7 @@ setTimeout(() => {
         items: '.js-list:not(.js-list-composer)',
         placeholder: 'list placeholder',
         distance: 7,
-        handle: '.js-list-handle',
+        handle: computeHandle(),
         disabled: !Utils.canModifyBoard(),
         start(evt, ui) {
           ui.helper.css('z-index', 1000);
@@ -973,6 +999,13 @@ setTimeout(() => {
           });
         }
       });
+      // Reactively adjust handle when setting changes
+      Tracker.autorun(() => {
+        const newHandle = computeHandle();
+        if ($listsGroup.data('uiSortable') || $listsGroup.data('sortable')) {
+          try { $listsGroup.sortable('option', 'handle', newHandle); } catch (e) {}
+        }
+      });
     }
   });
 }, 1000);
@@ -1018,6 +1051,9 @@ BlazeComponent.extendComponent({
     
     // Wait for DOM to be ready
     setTimeout(() => {
+      const handleSelector = Utils.isTouchScreenOrShowDesktopDragHandles()
+        ? '.js-list-handle'
+        : '.js-list-header';
       const $lists = this.$('.js-list');
       
       const $parent = $lists.parent();
@@ -1041,7 +1077,7 @@ BlazeComponent.extendComponent({
           items: '.js-list:not(.js-list-composer)',
           placeholder: 'list placeholder',
           distance: 7,
-          handle: '.js-list-handle',
+          handle: handleSelector,
           disabled: !Utils.canModifyBoard(),
           start(evt, ui) {
             ui.helper.css('z-index', 1000);
@@ -1054,6 +1090,15 @@ BlazeComponent.extendComponent({
             boardComponent.setIsDragging(false);
           }
         });
+        // Reactively update handle when user toggles desktop drag handles
+        this.autorun(() => {
+          const newHandle = Utils.isTouchScreenOrShowDesktopDragHandles()
+            ? '.js-list-handle'
+            : '.js-list-header';
+          if ($parent.data('uiSortable') || $parent.data('sortable')) {
+            try { $parent.sortable('option', 'handle', newHandle); } catch (e) {}
+          }
+        });
       } else {
       }
     }, 100);

+ 121 - 6
client/lib/utils.js

@@ -79,13 +79,21 @@ Utils = {
   },
 
   getMobileMode() {
+    // Check localStorage first - user's explicit preference takes priority
+    const stored = localStorage.getItem('wekan-mobile-mode');
+    if (stored !== null) {
+      return stored === 'true';
+    }
+    
+    // Then check user profile
     const user = ReactiveCache.getCurrentUser();
     if (user && user.profile && user.profile.mobileMode !== undefined) {
       return user.profile.mobileMode;
     }
-    // For non-logged-in users, check localStorage
-    const stored = localStorage.getItem('wekan-mobile-mode');
-    return stored ? stored === 'true' : false;
+    
+    // Default to mobile mode for iPhone/iPod
+    const isIPhone = /iPhone|iPod/i.test(navigator.userAgent);
+    return isIPhone;
   },
 
   setMobileMode(enabled) {
@@ -93,13 +101,41 @@ Utils = {
     if (user) {
       // Update user profile
       user.setMobileMode(enabled);
-    } else {
-      // Store in localStorage for non-logged-in users
-      localStorage.setItem('wekan-mobile-mode', enabled.toString());
     }
+    // Always store in localStorage for persistence across sessions
+    localStorage.setItem('wekan-mobile-mode', enabled.toString());
     Utils.applyMobileMode(enabled);
     // Trigger reactive updates for UI components
     Session.set('wekan-mobile-mode', enabled);
+    // Re-apply zoom level to ensure proper rendering
+    const zoomLevel = Utils.getZoomLevel();
+    Utils.applyZoomLevel(zoomLevel);
+  },
+
+  getCardZoom() {
+    const user = ReactiveCache.getCurrentUser();
+    if (user && user.profile && user.profile.cardZoom !== undefined) {
+      return user.profile.cardZoom;
+    }
+    const stored = localStorage.getItem('wekan-card-zoom');
+    return stored ? parseFloat(stored) : 1.0;
+  },
+
+  setCardZoom(level) {
+    const user = ReactiveCache.getCurrentUser();
+    if (user) {
+      user.setCardZoom(level);
+    }
+    localStorage.setItem('wekan-card-zoom', level.toString());
+    Utils.applyCardZoom(level);
+    Session.set('wekan-card-zoom', level);
+  },
+
+  applyCardZoom(level) {
+    const cardDetails = document.querySelector('.card-details');
+    if (cardDetails) {
+      cardDetails.style.fontSize = `${level}em`;
+    }
   },
 
   applyZoomLevel(level) {
@@ -301,6 +337,85 @@ Utils = {
     }
   },
 
+  getListCollapseState(list) {
+    if (!list) return false;
+    const key = `collapsedList-${list._id}`;
+    const sessionVal = Session.get(key);
+    if (typeof sessionVal === 'boolean') {
+      return sessionVal;
+    }
+
+    const user = ReactiveCache.getCurrentUser();
+    let stored = null;
+    if (user && user.getCollapsedListFromStorage) {
+      stored = user.getCollapsedListFromStorage(list.boardId, list._id);
+    } else if (Users.getPublicCollapsedList) {
+      stored = Users.getPublicCollapsedList(list.boardId, list._id);
+    }
+
+    if (typeof stored === 'boolean') {
+      Session.setDefault(key, stored);
+      return stored;
+    }
+
+    const fallback = typeof list.collapsed === 'boolean' ? list.collapsed : false;
+    Session.setDefault(key, fallback);
+    return fallback;
+  },
+
+  setListCollapseState(list, collapsed) {
+    if (!list) return;
+    const key = `collapsedList-${list._id}`;
+    Session.set(key, !!collapsed);
+    const user = ReactiveCache.getCurrentUser();
+    if (user) {
+      Meteor.call('setListCollapsedState', list.boardId, list._id, !!collapsed);
+    } else if (Users.setPublicCollapsedList) {
+      Users.setPublicCollapsedList(list.boardId, list._id, !!collapsed);
+    }
+  },
+
+  getSwimlaneCollapseState(swimlane) {
+    if (!swimlane) return false;
+    const key = `collapsedSwimlane-${swimlane._id}`;
+    const sessionVal = Session.get(key);
+    if (typeof sessionVal === 'boolean') {
+      return sessionVal;
+    }
+
+    const user = ReactiveCache.getCurrentUser();
+    let stored = null;
+    if (user && user.getCollapsedSwimlaneFromStorage) {
+      stored = user.getCollapsedSwimlaneFromStorage(
+        swimlane.boardId,
+        swimlane._id,
+      );
+    } else if (Users.getPublicCollapsedSwimlane) {
+      stored = Users.getPublicCollapsedSwimlane(swimlane.boardId, swimlane._id);
+    }
+
+    if (typeof stored === 'boolean') {
+      Session.setDefault(key, stored);
+      return stored;
+    }
+
+    const fallback = typeof swimlane.collapsed === 'boolean' ? swimlane.collapsed : false;
+    Session.setDefault(key, fallback);
+    return fallback;
+  },
+
+  setSwimlaneCollapseState(swimlane, collapsed) {
+    if (!swimlane) return;
+    const key = `collapsedSwimlane-${swimlane._id}`;
+    Session.set(key, !!collapsed);
+    const user = ReactiveCache.getCurrentUser();
+    if (user) {
+      Meteor.call('setSwimlaneCollapsedState', swimlane.boardId, swimlane._id, !!collapsed);
+    } else if (Users.setPublicCollapsedSwimlane) {
+      Users.setPublicCollapsedSwimlane(swimlane.boardId, swimlane._id, !!collapsed);
+    }
+  },
+
   myCardsSort() {
     let sort = window.localStorage.getItem('myCardsSort');
 

+ 10 - 0
config/router.js

@@ -165,6 +165,16 @@ FlowRouter.route('/b/:boardId/:slug/:cardId', {
     Session.set('currentCard', params.cardId);
     Session.set('popupCardId', null);
     Session.set('popupCardBoardId', null);
+    
+    // In desktop mode, add to openCards array to support multiple cards
+    const isMobile = Utils.getMobileMode();
+    if (!isMobile) {
+      const openCards = Session.get('openCards') || [];
+      if (!openCards.includes(params.cardId)) {
+        openCards.push(params.cardId);
+        Session.set('openCards', openCards);
+      }
+    }
 
     Utils.manageCustomUI();
     Utils.manageMatomo();

+ 17 - 0
models/lists.js

@@ -297,6 +297,23 @@ Lists.helpers({
   },
 
   isCollapsed() {
+    if (Meteor.isClient) {
+      const user = ReactiveCache.getCurrentUser();
+      // Logged-in users: prefer profile/cookie-backed state
+      if (user && user.getCollapsedListFromStorage) {
+        const stored = user.getCollapsedListFromStorage(this.boardId, this._id);
+        if (typeof stored === 'boolean') {
+          return stored;
+        }
+      }
+      // Public users: fallback to cookie if available
+      if (!user && Users.getPublicCollapsedList) {
+        const stored = Users.getPublicCollapsedList(this.boardId, this._id);
+        if (typeof stored === 'boolean') {
+          return stored;
+        }
+      }
+    }
     return this.collapsed === true;
   },
 

+ 15 - 0
models/swimlanes.js

@@ -246,6 +246,21 @@ Swimlanes.helpers({
   },
 
   isCollapsed() {
+    if (Meteor.isClient) {
+      const user = ReactiveCache.getCurrentUser();
+      if (user && user.getCollapsedSwimlaneFromStorage) {
+        const stored = user.getCollapsedSwimlaneFromStorage(this.boardId, this._id);
+        if (typeof stored === 'boolean') {
+          return stored;
+        }
+      }
+      if (!user && Users.getPublicCollapsedSwimlane) {
+        const stored = Users.getPublicCollapsedSwimlane(this.boardId, this._id);
+        if (typeof stored === 'boolean') {
+          return stored;
+        }
+      }
+    }
     return this.collapsed === true;
   },
 

+ 323 - 1
models/users.js

@@ -11,6 +11,83 @@ const isSandstorm =
   Meteor.settings && Meteor.settings.public && Meteor.settings.public.sandstorm;
 Users = Meteor.users;
 
+// Public-board collapse persistence helpers (cookie-based for non-logged-in users)
+if (Meteor.isClient) {
+  const readCookieMap = name => {
+    try {
+      const stored = typeof document !== 'undefined' ? document.cookie : '';
+      const cookies = stored.split(';').map(c => c.trim());
+      let json = '{}';
+      for (const c of cookies) {
+        if (c.startsWith(name + '=')) {
+          json = decodeURIComponent(c.substring(name.length + 1));
+          break;
+        }
+      }
+      return JSON.parse(json || '{}');
+    } catch (e) {
+      console.warn('Error parsing collapse cookie', name, e);
+      return {};
+    }
+  };
+
+  const writeCookieMap = (name, data) => {
+    try {
+      const serialized = encodeURIComponent(JSON.stringify(data || {}));
+      const maxAge = 60 * 60 * 24 * 365; // 1 year
+      document.cookie = `${name}=${serialized}; path=/; max-age=${maxAge}`;
+    } catch (e) {
+      console.warn('Error writing collapse cookie', name, e);
+    }
+  };
+
+  Users.getPublicCollapsedList = (boardId, listId) => {
+    if (!boardId || !listId) return null;
+    const data = readCookieMap('wekan-collapsed-lists');
+    if (data[boardId] && typeof data[boardId][listId] === 'boolean') {
+      return data[boardId][listId];
+    }
+    return null;
+  };
+
+  Users.setPublicCollapsedList = (boardId, listId, collapsed) => {
+    if (!boardId || !listId) return false;
+    const data = readCookieMap('wekan-collapsed-lists');
+    if (!data[boardId]) data[boardId] = {};
+    data[boardId][listId] = !!collapsed;
+    writeCookieMap('wekan-collapsed-lists', data);
+    return true;
+  };
+
+  Users.getPublicCollapsedSwimlane = (boardId, swimlaneId) => {
+    if (!boardId || !swimlaneId) return null;
+    const data = readCookieMap('wekan-collapsed-swimlanes');
+    if (data[boardId] && typeof data[boardId][swimlaneId] === 'boolean') {
+      return data[boardId][swimlaneId];
+    }
+    return null;
+  };
+
+  Users.setPublicCollapsedSwimlane = (boardId, swimlaneId, collapsed) => {
+    if (!boardId || !swimlaneId) return false;
+    const data = readCookieMap('wekan-collapsed-swimlanes');
+    if (!data[boardId]) data[boardId] = {};
+    data[boardId][swimlaneId] = !!collapsed;
+    writeCookieMap('wekan-collapsed-swimlanes', data);
+    return true;
+  };
+
+  Users.getPublicCardCollapsed = () => {
+    const data = readCookieMap('wekan-card-collapsed');
+    return typeof data.state === 'boolean' ? data.state : null;
+  };
+
+  Users.setPublicCardCollapsed = collapsed => {
+    writeCookieMap('wekan-card-collapsed', { state: !!collapsed });
+    return true;
+  };
+}
+
 const allowedSortValues = [
   '-modifiedAt',
   'modifiedAt',
@@ -187,6 +264,13 @@ Users.attachSchema(
       type: Boolean,
       optional: true,
     },
+    'profile.cardCollapsed': {
+      /**
+       * has user collapsed the card details?
+       */
+      type: Boolean,
+      optional: true,
+    },
     'profile.customFieldsGrid': {
       /**
        * has user at card Custom Fields have Grid (false) or one per row (true) layout?
@@ -476,6 +560,24 @@ Users.attachSchema(
       defaultValue: {},
       blackbox: true,
     },
+    'profile.collapsedLists': {
+      /**
+       * Per-user collapsed state for lists.
+       * profile[boardId][listId] = true|false
+       */
+      type: Object,
+      defaultValue: {},
+      blackbox: true,
+    },
+    'profile.collapsedSwimlanes': {
+      /**
+       * Per-user collapsed state for swimlanes.
+       * profile[boardId][swimlaneId] = true|false
+       */
+      type: Object,
+      defaultValue: {},
+      blackbox: true,
+    },
     'profile.keyboardShortcuts': {
       /**
        * User-specified state of keyboard shortcut activation.
@@ -522,6 +624,15 @@ Users.attachSchema(
       type: Boolean,
       defaultValue: false,
     },
+    'profile.cardZoom': {
+      /**
+       * User-specified zoom level for card details (1.0 = 100%, 1.5 = 150%, etc.)
+       */
+      type: Number,
+      defaultValue: 1.0,
+      min: 0.5,
+      max: 3.0,
+    },
     services: {
       /**
        * services field of the user
@@ -602,7 +713,7 @@ Users.attachSchema(
 );
 
 // Security helpers for user updates
-export const USER_UPDATE_ALLOWED_EXACT = ['username'];
+export const USER_UPDATE_ALLOWED_EXACT = ['username', 'profile'];
 export const USER_UPDATE_ALLOWED_PREFIXES = ['profile.'];
 export const USER_UPDATE_FORBIDDEN_PREFIXES = [
   'services',
@@ -1311,6 +1422,135 @@ Users.helpers({
       return false;
     }
   },
+  // Per-user collapsed state helpers for lists/swimlanes
+  getCollapsedList(boardId, listId) {
+    const { collapsedLists = {} } = this.profile || {};
+    if (collapsedLists[boardId] && typeof collapsedLists[boardId][listId] === 'boolean') {
+      return collapsedLists[boardId][listId];
+    }
+    return null;
+  },
+  getCollapsedSwimlane(boardId, swimlaneId) {
+    const { collapsedSwimlanes = {} } = this.profile || {};
+    if (collapsedSwimlanes[boardId] && typeof collapsedSwimlanes[boardId][swimlaneId] === 'boolean') {
+      return collapsedSwimlanes[boardId][swimlaneId];
+    }
+    return null;
+  },
+  setCollapsedListToStorage(boardId, listId, collapsed) {
+    // Logged-in users: save to profile
+    if (this._id) {
+      return this.setCollapsedList(boardId, listId, collapsed);
+    }
+    // Public users: save to cookie
+    try {
+      const name = 'wekan-collapsed-lists';
+      const stored = (typeof document !== 'undefined') ? document.cookie : '';
+      const cookies = stored.split(';').map(c => c.trim());
+      let json = '{}';
+      for (const c of cookies) {
+        if (c.startsWith(name + '=')) {
+          json = decodeURIComponent(c.substring(name.length + 1));
+          break;
+        }
+      }
+      let data = {};
+      try { data = JSON.parse(json || '{}'); } catch (e) { data = {}; }
+      if (!data[boardId]) data[boardId] = {};
+      data[boardId][listId] = !!collapsed;
+      const serialized = encodeURIComponent(JSON.stringify(data));
+      const maxAge = 60 * 60 * 24 * 365; // 1 year
+      document.cookie = `${name}=${serialized}; path=/; max-age=${maxAge}`;
+      return true;
+    } catch (e) {
+      console.warn('Error saving collapsed list to cookie:', e);
+      return false;
+    }
+  },
+  getCollapsedListFromStorage(boardId, listId) {
+    // Logged-in users: read from profile
+    if (this._id) {
+      const v = this.getCollapsedList(boardId, listId);
+      return v;
+    }
+    // Public users: read from cookie
+    try {
+      const name = 'wekan-collapsed-lists';
+      const stored = (typeof document !== 'undefined') ? document.cookie : '';
+      const cookies = stored.split(';').map(c => c.trim());
+      let json = '{}';
+      for (const c of cookies) {
+        if (c.startsWith(name + '=')) {
+          json = decodeURIComponent(c.substring(name.length + 1));
+          break;
+        }
+      }
+      const data = JSON.parse(json || '{}');
+      if (data[boardId] && typeof data[boardId][listId] === 'boolean') {
+        return data[boardId][listId];
+      }
+    } catch (e) {
+      console.warn('Error reading collapsed list from cookie:', e);
+    }
+    return null;
+  },
+  setCollapsedSwimlaneToStorage(boardId, swimlaneId, collapsed) {
+    // Logged-in users: save to profile
+    if (this._id) {
+      return this.setCollapsedSwimlane(boardId, swimlaneId, collapsed);
+    }
+    // Public users: save to cookie
+    try {
+      const name = 'wekan-collapsed-swimlanes';
+      const stored = (typeof document !== 'undefined') ? document.cookie : '';
+      const cookies = stored.split(';').map(c => c.trim());
+      let json = '{}';
+      for (const c of cookies) {
+        if (c.startsWith(name + '=')) {
+          json = decodeURIComponent(c.substring(name.length + 1));
+          break;
+        }
+      }
+      let data = {};
+      try { data = JSON.parse(json || '{}'); } catch (e) { data = {}; }
+      if (!data[boardId]) data[boardId] = {};
+      data[boardId][swimlaneId] = !!collapsed;
+      const serialized = encodeURIComponent(JSON.stringify(data));
+      const maxAge = 60 * 60 * 24 * 365; // 1 year
+      document.cookie = `${name}=${serialized}; path=/; max-age=${maxAge}`;
+      return true;
+    } catch (e) {
+      console.warn('Error saving collapsed swimlane to cookie:', e);
+      return false;
+    }
+  },
+  getCollapsedSwimlaneFromStorage(boardId, swimlaneId) {
+    // Logged-in users: read from profile
+    if (this._id) {
+      const v = this.getCollapsedSwimlane(boardId, swimlaneId);
+      return v;
+    }
+    // Public users: read from cookie
+    try {
+      const name = 'wekan-collapsed-swimlanes';
+      const stored = (typeof document !== 'undefined') ? document.cookie : '';
+      const cookies = stored.split(';').map(c => c.trim());
+      let json = '{}';
+      for (const c of cookies) {
+        if (c.startsWith(name + '=')) {
+          json = decodeURIComponent(c.substring(name.length + 1));
+          break;
+        }
+      }
+      const data = JSON.parse(json || '{}');
+      if (data[boardId] && typeof data[boardId][swimlaneId] === 'boolean') {
+        return data[boardId][swimlaneId];
+      }
+    } catch (e) {
+      console.warn('Error reading collapsed swimlane from cookie:', e);
+    }
+    return null;
+  },
 });
 
 Users.mutations({
@@ -1485,6 +1725,14 @@ Users.mutations({
     };
   },
 
+  toggleCardCollapsed(value = false) {
+    return {
+      $set: {
+        'profile.cardCollapsed': !value,
+      },
+    };
+  },
+
   toggleLabelText(value = false) {
     return {
       $set: {
@@ -1621,6 +1869,26 @@ Users.mutations({
       },
     };
   },
+  setCollapsedList(boardId, listId, collapsed) {
+    const current = (this.profile && this.profile.collapsedLists) || {};
+    if (!current[boardId]) current[boardId] = {};
+    current[boardId][listId] = !!collapsed;
+    return {
+      $set: {
+        'profile.collapsedLists': current,
+      },
+    };
+  },
+  setCollapsedSwimlane(boardId, swimlaneId, collapsed) {
+    const current = (this.profile && this.profile.collapsedSwimlanes) || {};
+    if (!current[boardId]) current[boardId] = {};
+    current[boardId][swimlaneId] = !!collapsed;
+    return {
+      $set: {
+        'profile.collapsedSwimlanes': current,
+      },
+    };
+  },
 
   setZoomLevel(level) {
     return {
@@ -1637,6 +1905,14 @@ Users.mutations({
       },
     };
   },
+
+  setCardZoom(level) {
+    return {
+      $set: {
+        'profile.cardZoom': level,
+      },
+    };
+  },
 });
 
 Meteor.methods({
@@ -1809,6 +2085,11 @@ Meteor.methods({
     const user = ReactiveCache.getCurrentUser();
     user.toggleCardMaximized(user.hasCardMaximized());
   },
+  setCardCollapsed(value) {
+    check(value, Boolean);
+    if (!this.userId) throw new Meteor.Error('not-logged-in');
+    Users.update(this.userId, { $set: { 'profile.cardCollapsed': value } });
+  },
   toggleMinicardLabelText() {
     const user = ReactiveCache.getCurrentUser();
     user.toggleLabelText(user.hasHiddenMinicardLabelText());
@@ -1838,6 +2119,26 @@ Meteor.methods({
     user.setListWidth(boardId, listId, width);
     user.setListConstraint(boardId, listId, constraint);
   },
+  setListCollapsedState(boardId, listId, collapsed) {
+    check(boardId, String);
+    check(listId, String);
+    check(collapsed, Boolean);
+    if (!this.userId) {
+      throw new Meteor.Error('not-logged-in', 'User must be logged in');
+    }
+    const user = Users.findOne(this.userId);
+    if (!user) {
+      throw new Meteor.Error('user-not-found', 'User not found');
+    }
+    const current = (user.profile && user.profile.collapsedLists) || {};
+    if (!current[boardId]) current[boardId] = {};
+    current[boardId][listId] = !!collapsed;
+    Users.update(this.userId, {
+      $set: {
+        'profile.collapsedLists': current,
+      },
+    });
+  },
   applySwimlaneHeight(boardId, swimlaneId, height) {
     check(boardId, String);
     check(swimlaneId, String);
@@ -1846,6 +2147,27 @@ Meteor.methods({
     user.setSwimlaneHeight(boardId, swimlaneId, height);
   },
 
+  setSwimlaneCollapsedState(boardId, swimlaneId, collapsed) {
+    check(boardId, String);
+    check(swimlaneId, String);
+    check(collapsed, Boolean);
+    if (!this.userId) {
+      throw new Meteor.Error('not-logged-in', 'User must be logged in');
+    }
+    const user = Users.findOne(this.userId);
+    if (!user) {
+      throw new Meteor.Error('user-not-found', 'User not found');
+    }
+    const current = (user.profile && user.profile.collapsedSwimlanes) || {};
+    if (!current[boardId]) current[boardId] = {};
+    current[boardId][swimlaneId] = !!collapsed;
+    Users.update(this.userId, {
+      $set: {
+        'profile.collapsedSwimlanes': current,
+      },
+    });
+  },
+
   applySwimlaneHeightToStorage(boardId, swimlaneId, height) {
     check(boardId, String);
     check(swimlaneId, String);