Browse Source

Resize height of swimlane by dragging. Font Awesome to Unicode icons.

Thanks to xet7 !
Lauri Ojansivu 6 days ago
parent
commit
09631d6b0c

+ 1 - 1
CHANGELOG.md

@@ -192,7 +192,7 @@ and adds the following new features:
 
 - [Mobile one board per row. Board zoom size percent. Board toggle mobile/desktop mode. In Progress](https://github.com/wekan/wekan/commit/752699d1c2fb8ea9ff0f3ec9ae0b2b776443d826).
   Thanks to xet7.
-- [Drag any files from file manager to minicard or opened card.
+- Drag any files from file manager to minicard or opened card.
   [Part 1](https://github.com/wekan/wekan/commit/3e9481c5bd2c02ba501bd0a6ef1d1e6ce82bb1d9),
   [Part 2](https://github.com/wekan/wekan/commit/cdd7d69c660d0b6ac06b7b75d4f59985b8a9322a).
   Thanks to xet7.

+ 31 - 31
client/components/cards/minicard.jade

@@ -5,11 +5,11 @@ template(name="minicard")
     class="{{#if colorClass}}minicard-{{colorClass}}{{/if}}")
     if canModifyCard
       if isTouchScreenOrShowDesktopDragHandles
-        a.fa.fa-navicon.minicard-details-menu-with-handle.js-open-minicard-details-menu(title="{{_ 'cardDetailsActionsPopup-title'}}")
+        a.minicard-details-menu-with-handle.js-open-minicard-details-menu(title="{{_ 'cardDetailsActionsPopup-title'}}") | ☰
         .handle
-          .fa.fa-arrows
+          | ↔️
       else
-        a.fa.fa-navicon.minicard-details-menu.js-open-minicard-details-menu(title="{{_ 'cardDetailsActionsPopup-title'}}")
+        a.minicard-details-menu.js-open-minicard-details-menu(title="{{_ 'cardDetailsActionsPopup-title'}}") | ☰
     .dates
       if getReceived
         unless getStart
@@ -36,7 +36,7 @@ template(name="minicard")
     if hasActiveUploads
       .minicard-upload-progress
         .upload-progress-header
-          i.fa.fa-upload
+          | 📤
           span {{_ 'uploading-files'}} ({{uploadCount}})
         each uploads
           .upload-progress-item(class="{{#if $eq status 'error'}}upload-error{{/if}}")
@@ -45,11 +45,11 @@ template(name="minicard")
               .upload-progress-fill(style="width: {{progress}}%")
             if $eq status 'error'
               .upload-progress-error
-                i.fa.fa-exclamation-triangle
+                | ⚠️
                 span {{_ 'upload-failed'}}
             else if $eq status 'completed'
               .upload-progress-success
-                i.fa.fa-check
+                | ✅
                 span {{_ 'upload-completed'}}
 
     .minicard-title
@@ -61,12 +61,12 @@ template(name="minicard")
           | {{ parentCardName }}
       if isLinkedBoard
         a.js-linked-link
-          span.linked-icon.fa.fa-folder
+          span.linked-icon | 📁
       else if isLinkedCard
         a.js-linked-link
-          span.linked-icon.fa.fa-id-card
+          span.linked-icon | 🃏
       if getArchived
-        span.linked-icon.linked-archived.fa.fa-archive
+        span.linked-icon.linked-archived | 📦
       +viewer
         if currentBoard.allowsCardNumber
           span.card-number
@@ -147,7 +147,7 @@ template(name="minicard")
       if canModifyCard
         if comments.length
           .badge(title="{{_ 'card-comments-title' comments.length }}")
-            span.badge-icon.fa.fa-comment-o.badge-comment.badge-text
+            span.badge-icon.badge-comment.badge-text | 💬
               = ' '
               = comments.length
             //span.badge-comment.badge-text
@@ -155,36 +155,36 @@ template(name="minicard")
       if getDescription
         unless currentBoard.allowsDescriptionTextOnMinicard
           .badge.badge-state-image-only(title=getDescription)
-            span.badge-icon.fa.fa-align-left
+            span.badge-icon | 📝
       if getVoteQuestion
         .badge.badge-state-image-only(title=getVoteQuestion)
-          span.badge-icon.fa.fa-thumbs-up(class="{{#if voteState}}text-green{{/if}}")
+          span.badge-icon(class="{{#if voteState}}text-green{{/if}}") | 👍
           span.badge-text {{ voteCountPositive }}
-          span.badge-icon.fa.fa-thumbs-down(class="{{#if $eq voteState false}}text-red{{/if}}")
+          span.badge-icon(class="{{#if $eq voteState false}}text-red{{/if}}") | 👎
           span.badge-text {{ voteCountNegative }}
       if getPokerQuestion
         .badge.badge-state-image-only(title=getPokerQuestion)
-          span.badge-icon.fa.fa-check(class="{{#if pokerState}}text-green{{/if}}")
+          span.badge-icon(class="{{#if pokerState}}text-green{{/if}}") | ✅
           if expiredPoker
             span.badge-text {{ getPokerEstimation }}
       if attachments.length
         if currentBoard.allowsBadgeAttachmentOnMinicard
           .badge
-            span.badge-icon.fa.fa-paperclip
+            span.badge-icon | 📎
             span.badge-text= attachments.length
       if checklists.length
         .badge(class="{{#if checklistFinished}}is-finished{{/if}}")
-          span.badge-icon.fa.fa-check-square-o
+          span.badge-icon | ☑️
           span.badge-text.check-list-text {{checklistFinishedCount}}/{{checklistItemCount}}
       if allSubtasks.count
         .badge
-          span.badge-icon.fa.fa-sitemap
+          span.badge-icon | 🌐
           span.badge-text.check-list-text {{subtasksFinishedCount}}/{{allSubtasksCount}}
           //{{subtasksFinishedCount}}/{{subtasksCount}} does not work because when a subtaks is archived, the count goes down
       if currentBoard.allowsCardSortingByNumber
         if currentBoard.allowsCardSortingByNumberOnMinicard
           .badge
-            span.badge-icon.fa.fa-sort
+            span.badge-icon | 🔢
             span.badge-text.check-list-sort {{ sort }}
     if currentBoard.allowsDescriptionTextOnMinicard
       if getDescription
@@ -193,7 +193,7 @@ template(name="minicard")
             | {{ getDescription }}
     if shouldShowListOnMinicard
       .minicard-list-name
-        i.fa.fa-list
+        | 📋
         | {{ listName }}
     if $eq 'subtext-with-full-path' currentBoard.presentParentTask
       .parent-subtext
@@ -212,50 +212,50 @@ template(name="minicardDetailsActionsPopup")
     if canModifyCard
       li
         a.js-move-card
-          i.fa.fa-arrow-right
+          | ➡️
           | {{_ 'moveCardPopup-title'}}
       li
         a.js-copy-card
-          i.fa.fa-copy
+          | 📋
           | {{_ 'copyCardPopup-title'}}
       hr
       li
         a.js-archive
-          i.fa.fa-arrow-right
-          i.fa.fa-archive
+          | ➡️
+          | 📦
           | {{_ 'archive-card'}}
       hr
       li
         a.js-move-card-to-top
-          i.fa.fa-arrow-up
+          | ⬆️
           | {{_ 'moveCardToTop-title'}}
       li
         a.js-move-card-to-bottom
-          i.fa.fa-arrow-down
+          | ⬇️
           | {{_ 'moveCardToBottom-title'}}
       hr
       li
         a.js-add-labels
-          i.fa.fa-tags
+          | 🏷️
           | {{_ 'card-edit-labels'}}
       li
         a.js-due-date
-          i.fa.fa-sign-in
+          | 📥
           | {{_ 'editCardDueDatePopup-title'}}
       li
         a.js-set-card-color
-          i.fa.fa-paint-brush
+          | 🎨
           | {{_ 'setCardColorPopup-title'}}
       li
         a.js-link
-          i.fa.fa-link
+          | 🔗
           | {{_ 'link-card'}}
       li
       a.js-toggle-watch-card
         if isWatching
-          i.fa.fa-eye
+          | 👁️
           |  {{_ 'unwatch'}}
         else
-          i.fa.fa-eye-slash
+          | 👁️-slash
           |  {{_ 'watch'}}
 

+ 211 - 3
client/components/lists/list.css

@@ -21,6 +21,8 @@
   background: transparent;
   transition: background-color 0.2s ease;
   border-radius: 2px;
+  /* Ensure the handle is clickable */
+  pointer-events: auto;
 }
 
 .list-resize-handle:hover {
@@ -90,6 +92,8 @@
   width: var(--list-width, auto) !important;
   min-width: var(--list-width, auto) !important;
   max-width: var(--list-width, auto) !important;
+  /* Ensure the width is applied immediately */
+  overflow: visible !important;
 }
 
 body.list-resizing-active {
@@ -250,7 +254,70 @@ body.list-resizing-active * {
 }
 .list.list-collapsed {
   flex: none;
+  min-width: 60px;
+  max-width: 80px;
+  width: 60px;
+  min-height: 60vh;
+  height: 60vh;
+  overflow: visible;
+  position: relative;
+}
+.list.list-collapsed .list-header {
+  padding: 1vh 1.5vw 0.5vh;
+  min-height: 2.5vh !important;
+  height: auto !important;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: flex-start;
+  position: relative;
+  overflow: visible !important;
+  width: 100%;
+  max-width: 60px;
+  margin: 0 auto;
+}
+.list.list-collapsed .list-header .js-collapse {
+  margin: 0 auto 20px auto;
+  z-index: 10;
+  padding: 8px 12px;
+  font-size: 12px;
+  white-space: nowrap;
+  display: block;
+  width: fit-content;
 }
+.list.list-collapsed .list-header .list-rotated {
+  width: auto !important;
+  height: auto !important;
+  margin: 20px 0 0 0 !important;
+  position: relative !important;
+  overflow: visible !important;
+}
+
+.list.list-collapsed .list-header .list-rotated h2.list-header-name {
+  text-align: left;
+  overflow: visible;
+  white-space: nowrap;
+  display: block !important;
+  font-size: 12px;
+  line-height: 1.2;
+  color: #333;
+  background-color: rgba(255, 255, 255, 0.95);
+  border: 1px solid #ddd;
+  padding: 8px 4px;
+  border-radius: 4px;
+  margin: 0 auto;
+  width: 25vh;
+  height: 60vh;
+  position: absolute;
+  left: 50%;
+  top: 50%;
+  transform: translate(calc(-50% + 50px), -50%) rotate(0deg);
+  z-index: 10;
+  visibility: visible !important;
+  opacity: 1 !important;
+  pointer-events: none;
+}
+
 .list.list-composer .open-list-composer,
 .list .list-composer .open-list-composer {
   color: #8c8c8c;
@@ -334,11 +401,152 @@ body.list-resizing-active * {
   color: #a6a6a6;
   margin-right: 15px;
 }
-.list-header .list-header-uncollapse-left {
+.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;
+  cursor: pointer;
+  font-size: 14px;
+}
+.list-header .js-collapse:hover {
+  background-color: #e0e0e0;
+  color: #333;
+}
+.list.list-collapsed .list-header .js-collapse {
+  display: inline-block !important;
+  visibility: visible !important;
+  opacity: 1 !important;
 }
-.list-header .list-header-uncollapse-right {
-  color: #a6a6a6;
+
+/* Responsive adjustments for collapsed lists */
+@media (min-width: 768px) {
+  .list.list-collapsed {
+    min-width: 60px;
+    max-width: 80px;
+    width: 60px;
+    min-height: 60vh;
+    height: 60vh;
+  }
+  .list.list-collapsed .list-header {
+    max-width: 60px;
+    margin: 0 auto;
+    min-height: 2.5vh !important;
+    height: auto !important;
+  }
+  .list.list-collapsed .list-header .list-rotated {
+    width: auto !important;
+    height: auto !important;
+    margin: 20px 0 0 0 !important;
+    position: relative !important;
+  }
+  .list.list-collapsed .list-header .list-rotated h2.list-header-name {
+    width: 15vh;
+    font-size: 12px;
+    height: 30px;
+    line-height: 1.2;
+    padding: 8px 4px;
+    margin: 0 auto;
+    position: absolute;
+    left: 50%;
+    top: 50%;
+    transform: translate(calc(-50% + 50px), -50%) rotate(0deg);
+    text-align: left;
+    visibility: visible !important;
+    opacity: 1 !important;
+    display: block !important;
+    background-color: rgba(255, 255, 255, 0.95);
+    border: 1px solid #ddd;
+    color: #333;
+    z-index: 10;
+  }
+  .list.list-collapsed .list-header .js-collapse {
+    margin: 0 auto 20px auto;
+  }
+}
+
+@media (min-width: 1024px) {
+  .list.list-collapsed {
+    min-height: 60vh;
+    height: 60vh;
+  }
+  .list.list-collapsed .list-header {
+    min-height: 2.5vh !important;
+    height: auto !important;
+  }
+  .list.list-collapsed .list-header .list-rotated {
+    width: auto !important;
+    height: auto !important;
+    margin: 20px 0 0 0 !important;
+    position: relative !important;
+  }
+  .list.list-collapsed .list-header .list-rotated h2.list-header-name {
+    width: 15vh;
+    font-size: 12px;
+    height: 30px;
+    line-height: 1.2;
+    padding: 8px 4px;
+    margin: 0 auto;
+    position: absolute;
+    left: 50%;
+    top: 50%;
+    transform: translate(calc(-50% + 50px), -50%) rotate(0deg);
+    text-align: left;
+    visibility: visible !important;
+    opacity: 1 !important;
+    display: block !important;
+    background-color: rgba(255, 255, 255, 0.95);
+    border: 1px solid #ddd;
+    color: #333;
+    z-index: 10;
+  }
+  .list.list-collapsed .list-header .js-collapse {
+    margin: 0 auto 20px auto;
+  }
+}
+
+@media (min-width: 1200px) {
+  .list.list-collapsed {
+    min-height: 60vh;
+    height: 60vh;
+  }
+  .list.list-collapsed .list-header {
+    min-height: 2.5vh !important;
+    height: auto !important;
+  }
+  .list.list-collapsed .list-header .list-rotated {
+    width: auto !important;
+    height: auto !important;
+    margin: 20px 0 0 0 !important;
+    position: relative !important;
+  }
+  .list.list-collapsed .list-header .list-rotated h2.list-header-name {
+    width: 15vh;
+    font-size: 12px;
+    height: 30px;
+    line-height: 1.2;
+    padding: 8px 4px;
+    margin: 0 auto;
+    position: absolute;
+    left: 50%;
+    top: 50%;
+    transform: translate(calc(-50% + 50px), -50%) rotate(0deg);
+    text-align: left;
+    visibility: visible !important;
+    opacity: 1 !important;
+    display: block !important;
+    background-color: rgba(255, 255, 255, 0.95);
+    border: 1px solid #ddd;
+    color: #333;
+    z-index: 10;
+  }
+  .list.list-collapsed .list-header .js-collapse {
+    margin: 0 auto 20px auto;
+  }
 }
 .list-header .list-header-collapse {
   color: #a6a6a6;

+ 1 - 1
client/components/lists/list.jade

@@ -4,7 +4,7 @@ template(name='list')
                 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
+    .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}}")

+ 60 - 36
client/components/lists/list.js

@@ -24,7 +24,7 @@ BlazeComponent.extendComponent({
   onRendered() {
     const boardComponent = this.parentComponent().parentComponent();
 
-    // Initialize list resize functionality
+    // Initialize list resize functionality immediately
     this.initializeListResize();
 
     const itemsSelector = '.js-minicard:not(.placeholder, .js-card-composer)';
@@ -201,13 +201,15 @@ BlazeComponent.extendComponent({
   listWidth() {
     const user = ReactiveCache.getCurrentUser();
     const list = Template.currentData();
-    return user.getListWidth(list.boardId, list._id);
+    if (!user || !list) return 270; // Return default width if user or list is not available
+    return user.getListWidthFromStorage(list.boardId, list._id);
   },
 
   listConstraint() {
     const user = ReactiveCache.getCurrentUser();
     const list = Template.currentData();
-    return user.getListConstraint(list.boardId, list._id);
+    if (!user || !list) return 550; // Return default constraint if user or list is not available
+    return user.getListConstraintFromStorage(list.boardId, list._id);
   },
 
   autoWidth() {
@@ -217,12 +219,31 @@ BlazeComponent.extendComponent({
   },
 
   initializeListResize() {
+    // Check if we're still in a valid template context
+    if (!Template.currentData()) {
+      console.warn('No current template data available for list resize initialization');
+      return;
+    }
+    
     const list = Template.currentData();
     const $list = this.$('.js-list');
     const $resizeHandle = this.$('.js-list-resize-handle');
     
+    // Check if elements exist
+    if (!$list.length || !$resizeHandle.length) {
+      console.warn('List or resize handle not found, retrying in 100ms');
+      Meteor.setTimeout(() => {
+        if (!this.isDestroyed) {
+          this.initializeListResize();
+        }
+      }, 100);
+      return;
+    }
+    
+    
     // Only enable resize for non-collapsed, non-auto-width lists
-    if (list.collapsed || this.autoWidth()) {
+    const isAutoWidth = this.autoWidth();
+    if (list.collapsed || isAutoWidth) {
       $resizeHandle.hide();
       return;
     }
@@ -240,41 +261,41 @@ BlazeComponent.extendComponent({
       startX = e.pageX || e.originalEvent.touches[0].pageX;
       startWidth = $list.outerWidth();
       
+      
       // Add visual feedback
       $list.addClass('list-resizing');
       $('body').addClass('list-resizing-active');
       
+      
       // Prevent text selection during resize
       $('body').css('user-select', 'none');
       
       e.preventDefault();
+      e.stopPropagation();
     };
 
     const doResize = (e) => {
-      if (!isResizing) return;
+      if (!isResizing) {
+        return;
+      }
       
       const currentX = e.pageX || e.originalEvent.touches[0].pageX;
       const deltaX = currentX - startX;
       const newWidth = Math.max(minWidth, Math.min(maxWidth, startWidth + deltaX));
       
-      // Apply the new width immediately for real-time feedback using CSS custom properties
+      // Apply the new width immediately for real-time feedback
       $list[0].style.setProperty('--list-width', `${newWidth}px`);
-      $list.css({
-        'width': `${newWidth}px`,
-        'min-width': `${newWidth}px`,
-        'max-width': `${newWidth}px`,
-        'flex': 'none',
-        'flex-basis': 'auto',
-        'flex-grow': '0',
-        'flex-shrink': '0'
-      });
+      $list[0].style.setProperty('width', `${newWidth}px`);
+      $list[0].style.setProperty('min-width', `${newWidth}px`);
+      $list[0].style.setProperty('max-width', `${newWidth}px`);
+      $list[0].style.setProperty('flex', 'none');
+      $list[0].style.setProperty('flex-basis', 'auto');
+      $list[0].style.setProperty('flex-grow', '0');
+      $list[0].style.setProperty('flex-shrink', '0');
       
-      // Debug: log the width change
-      if (process.env.DEBUG === 'true') {
-        console.log(`Resizing list to ${newWidth}px`);
-      }
       
       e.preventDefault();
+      e.stopPropagation();
     };
 
     const stopResize = (e) => {
@@ -287,17 +308,15 @@ BlazeComponent.extendComponent({
       const deltaX = currentX - startX;
       const finalWidth = Math.max(minWidth, Math.min(maxWidth, startWidth + deltaX));
       
-      // Ensure the final width is applied using CSS custom properties
+      // Ensure the final width is applied
       $list[0].style.setProperty('--list-width', `${finalWidth}px`);
-      $list.css({
-        'width': `${finalWidth}px`,
-        'min-width': `${finalWidth}px`,
-        'max-width': `${finalWidth}px`,
-        'flex': 'none',
-        'flex-basis': 'auto',
-        'flex-grow': '0',
-        'flex-shrink': '0'
-      });
+      $list[0].style.setProperty('width', `${finalWidth}px`);
+      $list[0].style.setProperty('min-width', `${finalWidth}px`);
+      $list[0].style.setProperty('max-width', `${finalWidth}px`);
+      $list[0].style.setProperty('flex', 'none');
+      $list[0].style.setProperty('flex-basis', 'auto');
+      $list[0].style.setProperty('flex-grow', '0');
+      $list[0].style.setProperty('flex-shrink', '0');
       
       // Remove visual feedback but keep the width
       $list.removeClass('list-resizing');
@@ -311,16 +330,14 @@ BlazeComponent.extendComponent({
       const boardId = list.boardId;
       const listId = list._id;
       
-      // Use the same method as the hamburger menu
+      // Use the new storage method that handles both logged-in and non-logged-in users
       if (process.env.DEBUG === 'true') {
-        console.log(`Saving list width: ${finalWidth}px for list ${listId}`);
       }
-      Meteor.call('applyListWidth', boardId, listId, finalWidth, listConstraint, (error, result) => {
+      Meteor.call('applyListWidthToStorage', boardId, listId, finalWidth, listConstraint, (error, result) => {
         if (error) {
           console.error('Error saving list width:', error);
         } else {
           if (process.env.DEBUG === 'true') {
-            console.log('List width saved successfully:', result);
           }
         }
       });
@@ -334,9 +351,16 @@ BlazeComponent.extendComponent({
     $(document).on('mouseup', stopResize);
     
     // Touch events for mobile
-    $resizeHandle.on('touchstart', startResize);
-    $(document).on('touchmove', doResize);
-    $(document).on('touchend', stopResize);
+    $resizeHandle.on('touchstart', startResize, { passive: false });
+    $(document).on('touchmove', doResize, { passive: false });
+    $(document).on('touchend', stopResize, { passive: false });
+    
+    
+    // Prevent dragscroll interference
+    $resizeHandle.on('mousedown', (e) => {
+      e.stopPropagation();
+    });
+    
     
     // Reactively update resize handle visibility when auto-width changes
     component.autorun(() => {

+ 2 - 2
client/components/lists/listBody.jade

@@ -32,7 +32,7 @@ template(name="listBody")
             +addCardForm(listId=_id position="bottom")
           else
             a.open-minicard-composer.js-card-composer.js-open-inlined-form(title="{{_ 'add-card-to-bottom-of-list'}}")
-              i.fa.fa-plus
+              | ➕
 
 template(name="spinnerList")
   .sk-spinner.sk-spinner-list(
@@ -54,7 +54,7 @@ template(name="addCardForm")
 
   .add-controls.clearfix
     button.primary.confirm(type="submit") {{_ 'add'}}
-    a.fa.fa-times-thin.js-close-inlined-form
+    a.js-close-inlined-form | ❌
   .add-controls.clearfix
     unless currentBoard.isTemplatesBoard
       unless currentBoard.isTemplateBoard

+ 15 - 14
client/components/lists/listHeader.jade

@@ -10,9 +10,6 @@ template(name="listHeader")
           a.list-header-left-icon.fa.fa-angle-left.js-unselect-list
       else
         if collapsed
-          a.js-collapse(title="{{_ 'uncollapse'}}")
-            i.fa.fa-arrow-left.list-header-uncollapse-left
-            i.fa.fa-arrow-right.list-header-uncollapse-right
           if showCardsCountForList cards.length
             br
             span.cardCount {{cardsCount}}
@@ -29,6 +26,10 @@ template(name="listHeader")
         if showCardsCountForList cards.length
           span.cardCount {{cardsCount}} {{cardsCountForListIsOne cards.length}}
       else
+        if collapsed
+          a.js-collapse(title="{{_ 'uncollapse'}}")
+            | ⬅️
+            | ➡️
         div(class="{{#if collapsed}}list-rotated{{/if}}")
           h2.list-header-name(
             title="{{ moment modifiedAt 'LLL' }}"
@@ -45,32 +46,32 @@ template(name="listHeader")
       if isMiniScreen
         if currentList
           if isWatching
-            i.list-header-watch-icon.fa.fa-eye
+            i.list-header-watch-icon | 👁️
           div.list-header-menu
             unless currentUser.isCommentOnly
               if canSeeAddCard
-                a.js-add-card.fa.fa-plus.list-header-plus-top(title="{{_ 'add-card-to-top-of-list'}}")
-              a.fa.fa-navicon.js-open-list-menu(title="{{_ 'listActionPopup-title'}}")
+                a.js-add-card.list-header-plus-top(title="{{_ 'add-card-to-top-of-list'}}") | ➕
+              a.js-open-list-menu(title="{{_ 'listActionPopup-title'}}") | ☰
         else
-          a.list-header-menu-icon.fa.fa-angle-right.js-select-list
-          a.list-header-handle.handle.fa.fa-arrows.js-list-handle
+          a.list-header-menu-icon.js-select-list | ▶️
+          a.list-header-handle.handle.js-list-handle | ↔️
       else if currentUser.isBoardMember
         if isWatching
-          i.list-header-watch-icon.fa.fa-eye
+          i.list-header-watch-icon | 👁️
         unless collapsed
           div.list-header-menu
             unless currentUser.isCommentOnly
               //if isBoardAdmin
               //  a.fa.js-list-star.list-header-plus-top(class="fa-star{{#unless starred}}-o{{/unless}}")
               if canSeeAddCard
-                a.js-add-card.fa.fa-plus.list-header-plus-top(title="{{_ 'add-card-to-top-of-list'}}")
+                a.js-add-card.list-header-plus-top(title="{{_ 'add-card-to-top-of-list'}}") | ➕
                 a.js-collapse(title="{{_ 'collapse'}}")
-                  i.fa.fa-arrow-right.list-header-collapse-right
-                  i.fa.fa-arrow-left.list-header-collapse-left
-                a.fa.fa-navicon.js-open-list-menu(title="{{_ 'listActionPopup-title'}}")
+                  | ⬅️
+                  | ➡️
+                a.js-open-list-menu(title="{{_ 'listActionPopup-title'}}") | ☰
             if currentUser.isBoardAdmin
               if isTouchScreenOrShowDesktopDragHandles
-                a.list-header-handle.handle.fa.fa-arrows.js-list-handle
+                a.list-header-handle.handle.js-list-handle | ↔️
 
 template(name="editListTitleForm")
   .list-composer

+ 38 - 39
client/components/sidebar/sidebar.jade

@@ -7,14 +7,14 @@ template(name="sidebar")
     .sidebar-actions
       .sidebar-shortcuts
         a.sidebar-btn.js-shortcuts(title="{{_ 'keyboard-shortcuts' }}")
-          i.fa.fa-keyboard-o
+          | ⌨️
           span {{_ 'keyboard-shortcuts' }}
         a.sidebar-btn.js-keyboard-shortcuts-toggle(
           title="{{#if isKeyboardShortcuts}}{{_ 'keyboard-shortcuts-enabled'}}{{else}}{{_ 'keyboard-shortcuts-disabled'}}{{/if}}")
-          i.fa(class="fa-solid fa-{{#if isKeyboardShortcuts}}check-square-o{{else}}ban{{/if}}")
+          | {{#if isKeyboardShortcuts}}✅{{else}}🚫{{/if}}
       if isAccessibilityEnabled
         a.sidebar-accessibility
-          i.fa.fa-universal-access
+          | ♿
           span {{_ 'accessibility'}}
       a.sidebar-xmark.js-close-sidebar ✕
     .sidebar-content.js-board-sidebar-content
@@ -22,7 +22,7 @@ template(name="sidebar")
       //  i.fa.fa-navicon
       unless isDefaultView
         h2
-          a.fa.fa-chevron-left.js-back-home
+          a.js-back-home | ⬅️
           = getViewTitle
       if isOpen
         +Template.dynamic(template=getViewTemplate)
@@ -51,7 +51,7 @@ template(name='homeSidebar')
   hr
   unless currentUser.isNoComments
     h3.activity-title
-      i.fa.fa-comments-o
+      | 💬
       | {{_ 'activities'}}
 
       .material-toggle-switch(title="{{_ 'show-activities'}}")
@@ -67,11 +67,11 @@ template(name="membersWidget")
     unless currentUser.isWorker
       h3
         a.board-header-btn.js-open-board-menu(title="{{_ 'boardMenuPopup-title'}}")
-          i.board-header-btn-icon.fa.fa-cog
+          | ⚙️
           | {{_ 'boardMenuPopup-title'}}
   hr
   h3
-    i.fa.fa-users
+    | 👥
     | {{_ 'members'}}
   +basicTabs(tabs=tabs)
       +tabContent(slug="people")
@@ -83,15 +83,15 @@ template(name="membersWidget")
           if isSandstorm
             if currentUser.isBoardMember
               a.member.add-member.sandstorm-powerbox-request-identity(title="{{_ 'add-members'}}")
-                i.fa.fa-plus
+                | ➕
           else if currentUser.isBoardAdmin
             a.member.add-member.js-manage-board-members(title="{{_ 'add-members'}}")
-              i.fa.fa-plus
+              | ➕
           .clearfix
           if isInvited
             hr
             p
-              i.fa.fa-exclamation-circle
+              | ⚠️
               | {{_ 'just-invited'}}
             button.js-member-invite-accept.primary {{_ 'accept'}}
             button.js-member-invite-decline {{_ 'decline'}}
@@ -127,7 +127,7 @@ template(name="boardOrgGeneral")
         th
           if currentUser.isBoardAdmin
             a.member.orgOrTeamMember.add-member.js-manage-board-addOrg(title="{{_ 'add-members'}}")
-              i.addTeamFaPlus.fa.fa-plus
+              | ➕
             .divaddfaplusminus
               | {{_ 'add'}}
       each org in currentBoard.activeOrgs
@@ -148,7 +148,7 @@ template(name="boardTeamGeneral")
         th
           if currentUser.isBoardAdmin
             a.member.orgOrTeamMember.add-member.js-manage-board-addTeam(title="{{_ 'add-members'}}")
-              i.addTeamFaPlus.fa.fa-plus
+              | ➕
             .divaddfaplusminus
               | {{_ 'add'}}
       each currentBoard.activeTeams
@@ -161,7 +161,7 @@ template(name="boardChangeColorPopup")
         span.background-box(class="board-color-{{this}}")
           span {{this}}
           if isSelected
-            i.fa.fa-check
+            | ✅
 
 template(name="boardChangeBackgroundImagePopup")
   form
@@ -423,7 +423,7 @@ template(name="chooseBoardSource")
 template(name="archiveBoardPopup")
   p {{_ 'close-board-pop'}}
   button.js-confirm.negate.full(type="submit")
-    i.fa.fa-archive
+    | 📦
     | {{_ 'archive'}}
 
 template(name="outgoingWebhooksPopup")
@@ -459,25 +459,25 @@ template(name="boardMenuPopup")
     if currentUser.isBoardAdmin
       li
         a.js-open-rules-view(title="{{_ 'rules'}}")
-          i.fa.fa-magic
+          | ✨
           | {{_ 'rules'}}
     if currentUser.isBoardAdmin
       li
         a.js-custom-fields
-          i.fa.fa-list-alt
+          | 📝
           | {{_ 'custom-fields'}}
     li
       a.js-open-archives
-        i.fa.fa-archive
+        | 📦
         | {{_ 'archived-items'}}
     if currentUser.isBoardAdmin
       li
         a.js-change-board-color
-          i.fa.fa-paint-brush
+          | 🎨
           | {{_ 'board-change-color'}}
       li
         a.js-change-background-image
-          i.fa.fa-picture-o
+          | 🖼️
           | {{_ 'board-change-background-image'}}
     //Bug Board icons random dance https://github.com/wekan/wekan/issues/4214
     //if currentUser.isBoardAdmin
@@ -492,20 +492,20 @@ template(name="boardMenuPopup")
     if withApi
       li
         a.js-export-board
-          i.fa.fa-share-alt
+          | 📤
           | {{_ 'export-board'}}
     if currentUser.isBoardAdmin
       li
         a.js-outgoing-webhooks
-          i.fa.fa-globe
+          | 🌐
           | {{_ 'outgoing-webhooks'}}
       li
         a.js-card-settings
-          i.fa.fa-id-card-o
+          | 🃏
           | {{_ 'card-settings'}}
       li
         a.js-subtask-settings
-          i.fa.fa-sitemap
+          | 🌐
           | {{_ 'subtask-settings'}}
   unless currentBoard.isTemplatesBoard
     if currentUser.isBoardAdmin
@@ -513,41 +513,40 @@ template(name="boardMenuPopup")
       ul.pop-over-list
         li
           a.js-archive-board
-            i.fa.fa-arrow-right
-            i.fa.fa-archive
+            | ➡️📦
             | {{_ 'archive-board'}}
 
 template(name="exportBoard")
   ul.pop-over-list
     li
       a.download-json-link(href="{{exportUrl}}", download="{{exportJsonFilename}}")
-        i.fa.fa-share-alt
+        | 📤
         | {{_ 'export-board-json'}}
     li
       a(href="{{exportUrlExcel}}", download="{{exportFilenameExcel}}")
-        i.fa.fa-share-alt
+        | 📤
         | {{_ 'export-board-excel'}}
     li
       a(href="{{exportCsvUrl}}", download="{{exportCsvFilename}}")
-        i.fa.fa-share-alt
+        | 📤
         | {{_ 'export-board-csv'}} ,
     li
       a(href="{{exportScsvUrl}}", download="{{exportCsvFilename}}")
-        i.fa.fa-share-alt
+        | 📤
         | {{_ 'export-board-csv'}} ;
     li
       a(href="{{exportTsvUrl}}", download="{{exportTsvFilename}}")
-        i.fa.fa-share-alt
+        | 📤
         | {{_ 'export-board-tsv'}}
     li
       a.html-export-board
-        i.fa.fa-archive
+        | 📦
         | {{_ 'export-board-html'}}
 
 template(name="labelsWidget")
   .board-widget.board-widget-labels
     h3
-      i.fa.fa-tags
+      | 🏷️
       | {{_ 'labels'}}
     .board-widget-content
       each currentBoard.labels
@@ -558,7 +557,7 @@ template(name="labelsWidget")
                 = name
       if currentUser.isBoardAdmin
         a.card-label.add-label.js-add-label(title="{{_ 'label-create'}}")
-          i.fa.fa-plus
+          | ➕
 
 template(name="memberPopup")
   .board-member-menu
@@ -570,7 +569,7 @@ template(name="memberPopup")
         p.quiet @#{user.username}
         if isInvited
           p
-            i.fa.fa-exclamation-circle
+            | ⚠️
             | {{_ 'not-accepted-yet'}}
 
     ul.pop-over-list
@@ -665,31 +664,31 @@ template(name="changePermissionsPopup")
       a(class="{{#if isLastAdmin}}disabled{{else}}js-set-admin{{/if}}")
         | {{_ 'admin'}}
         if isAdmin
-          i.fa.fa-check
+          | ✅
         span.sub-name {{_ 'admin-desc'}}
     li
       a(class="{{#if isLastAdmin}}disabled{{else}}js-set-normal{{/if}}")
         | {{_ 'normal'}}
         if isNormal
-          i.fa.fa-check
+          | ✅
         span.sub-name {{_ 'normal-desc'}}
     li
       a(class="{{#if isLastAdmin}}disabled{{else}}js-set-no-comments{{/if}}")
         | {{_ 'no-comments'}}
         if isNoComments
-          i.fa.fa-check
+          | ✅
         span.sub-name {{_ 'no-comments-desc'}}
     li
       a(class="{{#if isLastAdmin}}disabled{{else}}js-set-comment-only{{/if}}")
         | {{_ 'comment-only'}}
         if isCommentOnly
-          i.fa.fa-check
+          | ✅
         span.sub-name {{_ 'comment-only-desc'}}
     li
       a(class="{{#if isLastAdmin}}disabled{{else}}js-set-worker{{/if}}")
         | {{_ 'worker'}}
         if isWorker
-          i.fa.fa-check
+          | ✅
         span.sub-name {{_ 'worker-desc'}}
   if isLastAdmin
     hr

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

@@ -24,10 +24,10 @@ template(name="swimlaneFixedHeader")
           | {{isTitleDefault title}}
   .swimlane-header-menu
     unless currentUser.isCommentOnly
-      a.js-open-add-swimlane-menu.swimlane-header-plus-icon
-        | ➕(title="{{_ 'add-swimlane'}}")
-      a.js-open-swimlane-menu
-        | ☰(title="{{_ 'swimlaneActionPopup-title'}}")
+      a.js-open-add-swimlane-menu.swimlane-header-plus-icon(title="{{_ 'add-swimlane'}}")
+        | ➕
+      a.js-open-swimlane-menu(title="{{_ 'swimlaneActionPopup-title'}}")
+        | ☰
       //// TODO: Collapse Swimlane: make button working, etc.
       //unless collapsed
       //  a.js-collapse-swimlane(title="{{_ 'collapse'}}")
@@ -99,7 +99,7 @@ template(name="setSwimlaneColorPopup")
         each colors
           span.card-label.palette-color.js-palette-color(class="card-details-{{color}}")
             if(isSelected color)
-              i.fa.fa-check
+              | ✅
     // Buttons aligned left too
     .flush-left
       button.primary.confirm.js-submit(style="margin-left:0") {{_ 'save'}}

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

@@ -8,6 +8,7 @@
   flex-direction: row;
   overflow: auto;
   max-height: 100%;
+  position: relative;
 }
 .swimlane-header-menu .swimlane-header-collapse-down {
   font-size: 50%;
@@ -234,3 +235,106 @@
   background: #4b0082 !important;
   color: #fff !important;
 }
+
+/* Swimlane resize handle */
+.swimlane-resize-handle {
+  position: absolute;
+  bottom: 0;
+  left: 0;
+  right: 0;
+  height: 8px;
+  background: transparent;
+  cursor: row-resize;
+  z-index: 20;
+  border-top: 2px solid transparent;
+  transition: all 0.2s ease;
+  border-radius: 2px;
+  /* Ensure the handle is clickable */
+  pointer-events: auto;
+}
+
+/* Show resize handle only on hover */
+.swimlane:hover .swimlane-resize-handle {
+  background: rgba(0, 0, 0, 0.1);
+  border-top-color: rgba(0, 0, 0, 0.2);
+}
+
+/* Add a subtle resize indicator line at the bottom of swimlane on hover */
+.swimlane:hover .swimlane-resize-handle::after {
+  content: '';
+  position: absolute;
+  bottom: 0;
+  left: 0;
+  right: 0;
+  height: 2px;
+  background: rgba(0, 123, 255, 0.3);
+  z-index: 21;
+  transition: all 0.2s ease;
+  border-radius: 1px;
+}
+
+/* Make the indicator line more prominent when hovering over the resize handle */
+.swimlane-resize-handle:hover::after {
+  background: rgba(0, 123, 255, 0.6) !important;
+  height: 3px !important;
+  box-shadow: 0 0 4px rgba(0, 123, 255, 0.2);
+}
+
+.swimlane-resize-handle:hover {
+  background: rgba(0, 123, 255, 0.4) !important;
+  border-top-color: #0079bf !important;
+  box-shadow: 0 0 4px rgba(0, 123, 255, 0.3);
+}
+
+.swimlane-resize-handle:active {
+  background: rgba(0, 123, 255, 0.6) !important;
+  border-top-color: #0079bf !important;
+  box-shadow: 0 0 6px rgba(0, 123, 255, 0.4);
+}
+
+/* Add a subtle indicator line */
+.swimlane-resize-handle::before {
+  content: '';
+  position: absolute;
+  left: 50%;
+  top: 50%;
+  transform: translate(-50%, -50%);
+  width: 20px;
+  height: 2px;
+  background: rgba(0, 123, 255, 0.6);
+  border-radius: 1px;
+  opacity: 0;
+  transition: opacity 0.2s ease;
+}
+
+.swimlane-resize-handle:hover::before {
+  opacity: 1;
+}
+
+/* Visual feedback during resize */
+.swimlane.swimlane-resizing {
+  transition: none !important;
+  box-shadow: 0 0 10px rgba(0, 123, 255, 0.3);
+  /* Ensure the swimlane maintains its new height during resize */
+  flex: none !important;
+  flex-basis: auto !important;
+  flex-grow: 0 !important;
+  flex-shrink: 0 !important;
+  /* Override any conflicting layout properties */
+  display: flex !important;
+  position: relative !important;
+  /* Force height to be respected */
+  height: var(--swimlane-height, auto) !important;
+  min-height: var(--swimlane-height, auto) !important;
+  max-height: var(--swimlane-height, auto) !important;
+  /* Ensure the height is applied immediately */
+  overflow: visible !important;
+}
+
+body.swimlane-resizing-active {
+  cursor: row-resize !important;
+}
+
+body.swimlane-resizing-active * {
+  cursor: row-resize !important;
+}

+ 2 - 1
client/components/swimlanes/swimlanes.jade

@@ -4,6 +4,7 @@ template(name="swimlane")
   unless collapseSwimlane
     .swimlane.js-lists.js-swimlane.dragscroll(id="swimlane-{{_id}}"
                                    style="height:{{swimlaneHeight}};")
+      .swimlane-resize-handle.js-swimlane-resize-handle.nodragscroll
       if isMiniScreen
         if currentListIsInThisSwimlane _id
           +list(currentList)
@@ -67,7 +68,7 @@ template(name="addListForm")
                     a.js-list-template {{_ 'template'}}
           else
             a.open-list-composer.js-open-inlined-form(title="{{_ 'add-list'}}")
-              i.fa.fa-plus
+              | ➕
 
 template(name="moveSwimlanePopup")
   unless currentUser.isWorker

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

@@ -247,10 +247,21 @@ BlazeComponent.extendComponent({
             Utils.isTouchScreenOrShowDesktopDragHandles()
               ? ['.js-list-handle', '.js-swimlane-header-handle']
               : ['.js-list-header'],
-          );
-
+          ).concat([
+            '.js-list-resize-handle',
+            '.js-swimlane-resize-handle'
+          ]);
+
+          const isResizeHandle = $(evt.target).closest('.js-list-resize-handle, .js-swimlane-resize-handle').length > 0;
+          const isInNoDragArea = $(evt.target).closest(noDragInside.join(',')).length > 0;
+          
+          if (isResizeHandle) {
+            console.log('Board drag prevented - resize handle clicked');
+            return;
+          }
+          
           if (
-            $(evt.target).closest(noDragInside.join(',')).length === 0 &&
+            !isInNoDragArea &&
             this.$('.swimlane').prop('clientHeight') > evt.offsetY
           ) {
             this._isDragging = true;
@@ -283,9 +294,150 @@ BlazeComponent.extendComponent({
   swimlaneHeight() {
     const user = ReactiveCache.getCurrentUser();
     const swimlane = Template.currentData();
-    const height = user.getSwimlaneHeight(swimlane.boardId, swimlane._id);
+    const height = user.getSwimlaneHeightFromStorage(swimlane.boardId, swimlane._id);
     return height == -1 ? "auto" : (height + 5 + "px");
   },
+
+  onRendered() {
+    // Initialize swimlane resize functionality immediately
+    this.initializeSwimlaneResize();
+  },
+
+  initializeSwimlaneResize() {
+    // Check if we're still in a valid template context
+    if (!Template.currentData()) {
+      console.warn('No current template data available for swimlane resize initialization');
+      return;
+    }
+    
+    const swimlane = Template.currentData();
+    const $swimlane = $(`#swimlane-${swimlane._id}`);
+    const $resizeHandle = $swimlane.find('.js-swimlane-resize-handle');
+    
+    // Check if elements exist
+    if (!$swimlane.length || !$resizeHandle.length) {
+      console.warn('Swimlane or resize handle not found, retrying in 100ms');
+      Meteor.setTimeout(() => {
+        if (!this.isDestroyed) {
+          this.initializeSwimlaneResize();
+        }
+      }, 100);
+      return;
+    }
+    
+    
+    if ($resizeHandle.length === 0) {
+      return;
+    }
+
+    let isResizing = false;
+    let startY = 0;
+    let startHeight = 0;
+    const minHeight = 100;
+    const maxHeight = 2000;
+
+    const startResize = (e) => {
+      isResizing = true;
+      startY = e.pageY || e.originalEvent.touches[0].pageY;
+      startHeight = parseInt($swimlane.css('height')) || 300;
+      
+      
+      $swimlane.addClass('swimlane-resizing');
+      $('body').addClass('swimlane-resizing-active');
+      $('body').css('user-select', 'none');
+      
+      
+      e.preventDefault();
+      e.stopPropagation();
+    };
+
+    const doResize = (e) => {
+      if (!isResizing) {
+        return;
+      }
+      
+      const currentY = e.pageY || e.originalEvent.touches[0].pageY;
+      const deltaY = currentY - startY;
+      const newHeight = Math.max(minHeight, Math.min(maxHeight, startHeight + deltaY));
+      
+      
+      // Apply the new height immediately for real-time feedback
+      $swimlane[0].style.setProperty('--swimlane-height', `${newHeight}px`);
+      $swimlane[0].style.setProperty('height', `${newHeight}px`);
+      $swimlane[0].style.setProperty('min-height', `${newHeight}px`);
+      $swimlane[0].style.setProperty('max-height', `${newHeight}px`);
+      $swimlane[0].style.setProperty('flex', 'none');
+      $swimlane[0].style.setProperty('flex-basis', 'auto');
+      $swimlane[0].style.setProperty('flex-grow', '0');
+      $swimlane[0].style.setProperty('flex-shrink', '0');
+      
+      
+      e.preventDefault();
+      e.stopPropagation();
+    };
+
+    const stopResize = (e) => {
+      if (!isResizing) return;
+      
+      isResizing = false;
+      
+      // Calculate final height
+      const currentY = e.pageY || e.originalEvent.touches[0].pageY;
+      const deltaY = currentY - startY;
+      const finalHeight = Math.max(minHeight, Math.min(maxHeight, startHeight + deltaY));
+      
+      // Ensure the final height is applied
+      $swimlane[0].style.setProperty('--swimlane-height', `${finalHeight}px`);
+      $swimlane[0].style.setProperty('height', `${finalHeight}px`);
+      $swimlane[0].style.setProperty('min-height', `${finalHeight}px`);
+      $swimlane[0].style.setProperty('max-height', `${finalHeight}px`);
+      $swimlane[0].style.setProperty('flex', 'none');
+      $swimlane[0].style.setProperty('flex-basis', 'auto');
+      $swimlane[0].style.setProperty('flex-grow', '0');
+      $swimlane[0].style.setProperty('flex-shrink', '0');
+      
+      // Remove visual feedback but keep the height
+      $swimlane.removeClass('swimlane-resizing');
+      $('body').removeClass('swimlane-resizing-active');
+      $('body').css('user-select', '');
+      
+      // Save the new height using the existing system
+      const boardId = swimlane.boardId;
+      const swimlaneId = swimlane._id;
+      
+      if (process.env.DEBUG === 'true') {
+      }
+      
+      // Use the new storage method that handles both logged-in and non-logged-in users
+      Meteor.call('applySwimlaneHeightToStorage', boardId, swimlaneId, finalHeight, (error, result) => {
+        if (error) {
+          console.error('Error saving swimlane height:', error);
+        } else {
+          if (process.env.DEBUG === 'true') {
+          }
+        }
+      });
+      
+      e.preventDefault();
+    };
+
+    // Mouse events
+    $resizeHandle.on('mousedown', startResize);
+    $(document).on('mousemove', doResize);
+    $(document).on('mouseup', stopResize);
+    
+    // Touch events for mobile
+    $resizeHandle.on('touchstart', startResize, { passive: false });
+    $(document).on('touchmove', doResize, { passive: false });
+    $(document).on('touchend', stopResize, { passive: false });
+    
+    
+    // Prevent dragscroll interference
+    $resizeHandle.on('mousedown', (e) => {
+      e.stopPropagation();
+    });
+    
+  },
 }).register('swimlane');
 
 BlazeComponent.extendComponent({

+ 2 - 2
client/components/users/userHeader.jade

@@ -173,7 +173,7 @@ template(name="changeLanguagePopup")
         a.js-set-language
           = name
           if isCurrentLanguage
-            i.fa.fa-check
+            | ✅
 
 template(name="changeSettingsPopup")
   ul.pop-over-list
@@ -186,7 +186,7 @@ template(name="changeSettingsPopup")
     unless currentUser.isWorker
       li
         label.bold.clear
-          i.fa.fa-sort-numeric-asc
+          | 🔢
           | {{_ 'show-cards-minimum-count'}}
         input#show-cards-count-at.inline-input.left(type="number" value="#{showCardsCountAt}" min="-1")
         label.bold.clear

+ 2 - 2
client/config/blazeHelpers.js

@@ -65,8 +65,8 @@ Blaze.registerHelper('isTouchScreenOrShowDesktopDragHandles', () =>
 
 Blaze.registerHelper('moment', (...args) => {
   args.pop(); // hash
-  const [date, format] = args;
-  return format(new Date(date), format ?? 'LLLL');
+  const [date, formatStr] = args;
+  return format(new Date(date), formatStr ?? 'LLLL');
 });
 
 Blaze.registerHelper('canModifyCard', () =>

+ 202 - 0
models/users.js

@@ -875,6 +875,52 @@ Users.helpers({
     }
   },
 
+  getSwimlaneHeightFromStorage(boardId, swimlaneId) {
+    // For logged-in users, get from profile
+    if (this._id) {
+      return this.getSwimlaneHeight(boardId, swimlaneId);
+    }
+    
+    // For non-logged-in users, get from localStorage
+    try {
+      const stored = localStorage.getItem('wekan-swimlane-heights');
+      if (stored) {
+        const heights = JSON.parse(stored);
+        if (heights[boardId] && heights[boardId][swimlaneId]) {
+          return heights[boardId][swimlaneId];
+        }
+      }
+    } catch (e) {
+      console.warn('Error reading swimlane heights from localStorage:', e);
+    }
+    
+    return -1;
+  },
+
+  setSwimlaneHeightToStorage(boardId, swimlaneId, height) {
+    // For logged-in users, save to profile
+    if (this._id) {
+      return this.setSwimlaneHeight(boardId, swimlaneId, height);
+    }
+    
+    // For non-logged-in users, save to localStorage
+    try {
+      const stored = localStorage.getItem('wekan-swimlane-heights');
+      let heights = stored ? JSON.parse(stored) : {};
+      
+      if (!heights[boardId]) {
+        heights[boardId] = {};
+      }
+      heights[boardId][swimlaneId] = height;
+      
+      localStorage.setItem('wekan-swimlane-heights', JSON.stringify(heights));
+      return true;
+    } catch (e) {
+      console.warn('Error saving swimlane height to localStorage:', e);
+      return false;
+    }
+  },
+
   /** returns all confirmed move and copy dialog field values
    * <li> the board, swimlane and list id is stored for each board
    */
@@ -1032,6 +1078,144 @@ Users.helpers({
       _id: this._id,
     });
   },
+
+  getListWidthFromStorage(boardId, listId) {
+    // For logged-in users, get from profile
+    if (this._id) {
+      return this.getListWidth(boardId, listId);
+    }
+    
+    // For non-logged-in users, get from localStorage
+    try {
+      const stored = localStorage.getItem('wekan-list-widths');
+      if (stored) {
+        const widths = JSON.parse(stored);
+        if (widths[boardId] && widths[boardId][listId]) {
+          return widths[boardId][listId];
+        }
+      }
+    } catch (e) {
+      console.warn('Error reading list widths from localStorage:', e);
+    }
+    
+    return 270; // Return default width instead of -1
+  },
+
+  setListWidthToStorage(boardId, listId, width) {
+    // For logged-in users, save to profile
+    if (this._id) {
+      return this.setListWidth(boardId, listId, width);
+    }
+    
+    // For non-logged-in users, save to localStorage
+    try {
+      const stored = localStorage.getItem('wekan-list-widths');
+      let widths = stored ? JSON.parse(stored) : {};
+      
+      if (!widths[boardId]) {
+        widths[boardId] = {};
+      }
+      widths[boardId][listId] = width;
+      
+      localStorage.setItem('wekan-list-widths', JSON.stringify(widths));
+      return true;
+    } catch (e) {
+      console.warn('Error saving list width to localStorage:', e);
+      return false;
+    }
+  },
+
+  getListConstraintFromStorage(boardId, listId) {
+    // For logged-in users, get from profile
+    if (this._id) {
+      return this.getListConstraint(boardId, listId);
+    }
+    
+    // For non-logged-in users, get from localStorage
+    try {
+      const stored = localStorage.getItem('wekan-list-constraints');
+      if (stored) {
+        const constraints = JSON.parse(stored);
+        if (constraints[boardId] && constraints[boardId][listId]) {
+          return constraints[boardId][listId];
+        }
+      }
+    } catch (e) {
+      console.warn('Error reading list constraints from localStorage:', e);
+    }
+    
+    return 550; // Return default constraint instead of -1
+  },
+
+  setListConstraintToStorage(boardId, listId, constraint) {
+    // For logged-in users, save to profile
+    if (this._id) {
+      return this.setListConstraint(boardId, listId, constraint);
+    }
+    
+    // For non-logged-in users, save to localStorage
+    try {
+      const stored = localStorage.getItem('wekan-list-constraints');
+      let constraints = stored ? JSON.parse(stored) : {};
+      
+      if (!constraints[boardId]) {
+        constraints[boardId] = {};
+      }
+      constraints[boardId][listId] = constraint;
+      
+      localStorage.setItem('wekan-list-constraints', JSON.stringify(constraints));
+      return true;
+    } catch (e) {
+      console.warn('Error saving list constraint to localStorage:', e);
+      return false;
+    }
+  },
+
+  getSwimlaneHeightFromStorage(boardId, swimlaneId) {
+    // For logged-in users, get from profile
+    if (this._id) {
+      return this.getSwimlaneHeight(boardId, swimlaneId);
+    }
+    
+    // For non-logged-in users, get from localStorage
+    try {
+      const stored = localStorage.getItem('wekan-swimlane-heights');
+      if (stored) {
+        const heights = JSON.parse(stored);
+        if (heights[boardId] && heights[boardId][swimlaneId]) {
+          return heights[boardId][swimlaneId];
+        }
+      }
+    } catch (e) {
+      console.warn('Error reading swimlane heights from localStorage:', e);
+    }
+    
+    return -1; // Return -1 if not found
+  },
+
+  setSwimlaneHeightToStorage(boardId, swimlaneId, height) {
+    // For logged-in users, save to profile
+    if (this._id) {
+      return this.setSwimlaneHeight(boardId, swimlaneId, height);
+    }
+    
+    // For non-logged-in users, save to localStorage
+    try {
+      const stored = localStorage.getItem('wekan-swimlane-heights');
+      let heights = stored ? JSON.parse(stored) : {};
+      
+      if (!heights[boardId]) {
+        heights[boardId] = {};
+      }
+      heights[boardId][swimlaneId] = height;
+      
+      localStorage.setItem('wekan-swimlane-heights', JSON.stringify(heights));
+      return true;
+    } catch (e) {
+      console.warn('Error saving swimlane height to localStorage:', e);
+      return false;
+    }
+  },
 });
 
 Users.mutations({
@@ -1429,6 +1613,24 @@ Meteor.methods({
     const user = ReactiveCache.getCurrentUser();
     user.setSwimlaneHeight(boardId, swimlaneId, height);
   },
+
+  applySwimlaneHeightToStorage(boardId, swimlaneId, height) {
+    check(boardId, String);
+    check(swimlaneId, String);
+    check(height, Number);
+    const user = ReactiveCache.getCurrentUser();
+    user.setSwimlaneHeightToStorage(boardId, swimlaneId, height);
+  },
+
+  applyListWidthToStorage(boardId, listId, width, constraint) {
+    check(boardId, String);
+    check(listId, String);
+    check(width, Number);
+    check(constraint, Number);
+    const user = ReactiveCache.getCurrentUser();
+    user.setListWidthToStorage(boardId, listId, width);
+    user.setListConstraintToStorage(boardId, listId, constraint);
+  },
   setZoomLevel(level) {
     check(level, Number);
     const user = ReactiveCache.getCurrentUser();

+ 1 - 6
package-lock.json

@@ -137,7 +137,7 @@
       "from": "git+https://github.com/wekan/dragscroll.git"
     },
     "@wekanteam/exceljs": {
-      "version": "git+https://github.com/wekan/exceljs.git#7d182abf83ddfb1a8f5a9592a0fdf60ef72f6686",
+      "version": "git+https://github.com/wekan/exceljs.git#e0229907e7a81bc3fe6daf4e42b1fdfbecdcb7cb",
       "from": "git+https://github.com/wekan/exceljs.git",
       "requires": {
         "archiver": "^5.0.0",
@@ -2517,11 +2517,6 @@
       "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
       "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="
     },
-    "moment": {
-      "version": "2.30.1",
-      "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz",
-      "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how=="
-    },
     "mongo-object": {
       "version": "3.0.1",
       "resolved": "https://registry.npmjs.org/mongo-object/-/mongo-object-3.0.1.tgz",