From ed4ebcfa763f66ecd69cae57df2c949a7a9a8a45 Mon Sep 17 00:00:00 2001
From: Ariver <shanghai3168@gmail.com>
Date: Sat, 13 Jun 2026 20:23:29 +0800
Subject: [PATCH] Prepare 7.9.1 usage tips release

---
 src/Apptag/AppGridCollectionView.swift |   97 +++++++++++++++++++------------
 src/Scripts/usage_tips_qa.sh           |   34 +++++++++--
 src/CHANGELOG.md                       |    8 ++
 src/Apptag/Info.plist                  |    4 
 src/Apptag/Localization/zh-Hans.json   |    2 
 src/Apptag/Localization/zh-Hant.json   |    2 
 6 files changed, 98 insertions(+), 49 deletions(-)

diff --git a/src/Apptag/AppGridCollectionView.swift b/src/Apptag/AppGridCollectionView.swift
index e5df3a5..ae2714e 100644
--- a/src/Apptag/AppGridCollectionView.swift
+++ b/src/Apptag/AppGridCollectionView.swift
@@ -38,8 +38,8 @@
 }
 
 enum AppGridUsageTipsMetrics {
-    static let barHeight: CGFloat = 84
-    static let reservedHeight: CGFloat = 128
+    static let barHeight: CGFloat = 136
+    static let reservedHeight: CGFloat = 176
     static let bottomMargin: CGFloat = 20
     static let horizontalInset: CGFloat = 24
     static let minWidth: CGFloat = 640
@@ -943,44 +943,42 @@
         let paddingRight: CGFloat = 16
         let iconSize: CGFloat = 24
         let iconTextGap: CGFloat = 10
-        let titleDetailGap: CGFloat = 22
-        let detailControlsGap: CGFloat = 24
+        let controlZoneWidth: CGFloat = 176
         let buttonGap: CGFloat = 6
         let buttonSize: CGFloat = 48
+        let buttonDotsGap: CGFloat = 6
+        let dotsHeight: CGFloat = 10
         let dotsWidth = dotsView.preferredWidth
         let fullTitleWidth = ceil(titleLabel.attributedStringValue.size().width)
 
-        let centerY = visualFrame.midY
+        let titleX = visualFrame.minX + paddingLeft + iconSize + iconTextGap
+        let buttonsWidth = buttonSize * 2 + buttonGap
+        let buttonsGroupMinX = visualFrame.maxX - paddingRight - buttonsWidth
+        let textRight = visualFrame.maxX - controlZoneWidth
+        let availableTextWidth = max(1, textRight - titleX)
+        let titleWidth = min(fullTitleWidth, availableTextWidth)
+        let titleHeight: CGFloat = 32
+        let detailHeight: CGFloat = 68
+        let lineGap: CGFloat = 8
+        let textBlockHeight = titleHeight + lineGap + detailHeight
+        let textBlockY = visualFrame.minY + max(0, (visualFrame.height - textBlockHeight) / 2)
+
         iconView.frame = NSRect(
             x: visualFrame.minX + paddingLeft,
-            y: centerY - iconSize / 2,
+            y: visualFrame.midY - iconSize / 2,
             width: iconSize,
             height: iconSize
         )
 
-        let titleX = iconView.frame.maxX + iconTextGap
-        let buttonsWidth = buttonSize * 2 + buttonGap
-        let trailingWidth = paddingRight + buttonsWidth + detailControlsGap
-        let availableTextWidth = max(1, visualFrame.maxX - titleX - titleDetailGap - trailingWidth)
-        let minimumReadableDetailWidth: CGFloat = 220
-        let titleWidth: CGFloat
-        if availableTextWidth >= fullTitleWidth + minimumReadableDetailWidth {
-            titleWidth = fullTitleWidth
-        } else if availableTextWidth >= fullTitleWidth {
-            titleWidth = fullTitleWidth
-        } else {
-            titleWidth = max(1, availableTextWidth)
-        }
-
         titleLabel.frame = NSRect(
             x: titleX,
-            y: visualFrame.minY,
+            y: textBlockY,
             width: titleWidth,
-            height: visualFrame.height
+            height: titleHeight
         )
 
-        let buttonY = visualFrame.minY + 10
-        let buttonsGroupMinX = visualFrame.maxX - paddingRight - buttonsWidth
+        let controlGroupHeight = buttonSize + buttonDotsGap + dotsHeight
+        let buttonY = visualFrame.minY + max(0, (visualFrame.height - controlGroupHeight) / 2)
         nextButton.frame = NSRect(
             x: buttonsGroupMinX + buttonSize + buttonGap,
             y: buttonY,
@@ -995,18 +993,18 @@
         )
         dotsView.frame = NSRect(
             x: buttonsGroupMinX + (buttonsWidth - dotsWidth) / 2,
-            y: previousButton.frame.maxY + 6,
+            y: previousButton.frame.maxY + buttonDotsGap,
             width: dotsWidth,
-            height: 10
+            height: dotsHeight
         )
 
-        let detailX = titleLabel.frame.maxX + titleDetailGap
-        let detailRight = previousButton.frame.minX - detailControlsGap
+        let detailX = titleX
+        let detailRight = textRight
         detailScrollView.frame = NSRect(
             x: detailX,
-            y: visualFrame.minY,
+            y: titleLabel.frame.maxY + lineGap,
             width: max(1, detailRight - detailX),
-            height: visualFrame.height
+            height: detailHeight
         )
         layoutDetailLabel()
     }
@@ -1048,10 +1046,10 @@
 
     func preferredWidth(maxAvailableWidth: CGFloat) -> CGFloat {
         let padding: CGFloat = 22 + 16
-        let fixedWidth: CGFloat = 24 + 10 + 22 + 24 + 48 + 6 + 48
+        let fixedWidth: CGFloat = 24 + 10 + 24 + 48 + 6 + 48
         let titleWidth = ceil(titleLabel.attributedStringValue.size().width)
-        let detailWidth = ceil(detailLabel.attributedStringValue.size().width)
-        let contentWidth = padding + fixedWidth + titleWidth + detailWidth
+        let detailWidth = measuredDetailLineWidth()
+        let contentWidth = padding + fixedWidth + max(titleWidth, detailWidth)
         let preferredWidth = max(AppGridUsageTipsMetrics.minWidth, contentWidth)
         return min(maxAvailableWidth, preferredWidth)
     }
@@ -1081,7 +1079,7 @@
         addSubview(iconView)
 
         configureLabel(titleLabel, font: titleFont, lineBreakMode: .byClipping)
-        configureLabel(detailLabel, font: detailFont, lineBreakMode: .byClipping)
+        configureDetailLabel()
 
         detailScrollView.drawsBackground = false
         detailScrollView.hasVerticalScroller = false
@@ -1112,6 +1110,19 @@
         label.lineBreakMode = lineBreakMode
         label.maximumNumberOfLines = 1
         label.font = font
+    }
+
+    private func configureDetailLabel() {
+        detailLabel.cell = NSTextFieldCell(textCell: "")
+        detailLabel.isEditable = false
+        detailLabel.isSelectable = false
+        detailLabel.drawsBackground = false
+        detailLabel.isBordered = false
+        detailLabel.lineBreakMode = .byWordWrapping
+        detailLabel.maximumNumberOfLines = 2
+        detailLabel.font = detailFont
+        detailLabel.cell?.usesSingleLineMode = false
+        detailLabel.cell?.wraps = true
     }
 
     private func claimInteractionFocus() {
@@ -1180,9 +1191,17 @@
     }
 
     private func layoutDetailLabel() {
-        let textWidth = detailLabel.attributedStringValue.size().width
-        let width = max(detailScrollView.contentView.bounds.width, ceil(textWidth) + 18)
+        let width = max(1, detailScrollView.contentView.bounds.width)
+        detailLabel.preferredMaxLayoutWidth = width
         detailLabel.frame = NSRect(x: 0, y: 0, width: width, height: detailScrollView.bounds.height)
+    }
+
+    private func measuredDetailLineWidth() -> CGFloat {
+        let attributes: [NSAttributedString.Key: Any] = [.font: detailFont]
+        let lineWidths = detailLabel.stringValue
+            .components(separatedBy: .newlines)
+            .map { NSAttributedString(string: $0, attributes: attributes).size().width }
+        return ceil(lineWidths.max() ?? detailLabel.attributedStringValue.size().width)
     }
 
     private func currentVisualFrame() -> NSRect {
@@ -1198,8 +1217,10 @@
 
     private func formattedTipDetail(_ text: String) -> String {
         text
-            .replacingOccurrences(of: " > ", with: " -> ")
-            .replacingOccurrences(of: ">", with: "->")
+            .replacingOccurrences(of: "\\s*(?:-->|->|>|→|>)\\s*", with: "\n", options: .regularExpression)
+            .replacingOccurrences(of: "[ \\t]{2,}", with: " ", options: .regularExpression)
+            .replacingOccurrences(of: "\\n{2,}", with: "\n", options: .regularExpression)
+            .trimmingCharacters(in: .whitespacesAndNewlines)
     }
 
     private func updateColors() {
diff --git a/src/Apptag/Info.plist b/src/Apptag/Info.plist
index 900e665..6493cc7 100644
--- a/src/Apptag/Info.plist
+++ b/src/Apptag/Info.plist
@@ -19,9 +19,9 @@
 	<key>CFBundlePackageType</key>
 	<string>APPL</string>
 	<key>CFBundleShortVersionString</key>
-	<string>7.9.0</string>
+	<string>7.9.1</string>
 	<key>CFBundleVersion</key>
-	<string>20260612.1121</string>
+	<string>20260613.2020</string>
 	<key>LSApplicationCategoryType</key>
 	<string>public.app-category.utilities</string>
 	<key>LSMinimumSystemVersion</key>
diff --git a/src/Apptag/Localization/zh-Hans.json b/src/Apptag/Localization/zh-Hans.json
index 27e243f..22ddaee 100644
--- a/src/Apptag/Localization/zh-Hans.json
+++ b/src/Apptag/Localization/zh-Hans.json
@@ -221,7 +221,7 @@
   "usageTips.tip4.title": "技巧4-复制应用:",
   "usageTips.tip4.detail": "长按应用图标启动拖动 > 按住 Alt/Option 拖动到容器是复制",
   "usageTips.tip5.title": "技巧5-解除标签:",
-  "usageTips.tip5.detail": "长按应用图标启动拖动 > 把应用拖动到容器间的空白处",
+  "usageTips.tip5.detail": "长按应用图标启动拖动 > 拖动到容器间的空白处",
   "usageTips.tip6.title": "技巧6-标签排序:",
   "usageTips.tip6.detail": "长按标签启动拖动 > 拖动到目标位置,放手即可",
   "usageTips.tip7.title": "技巧7-应用排序:",
diff --git a/src/Apptag/Localization/zh-Hant.json b/src/Apptag/Localization/zh-Hant.json
index 05b30ac..6ac7032 100644
--- a/src/Apptag/Localization/zh-Hant.json
+++ b/src/Apptag/Localization/zh-Hant.json
@@ -221,7 +221,7 @@
   "usageTips.tip4.title": "技巧4-複製應用:",
   "usageTips.tip4.detail": "長按應用程式圖示開始拖曳 > 按住 Alt/Option 拖到容器即可複製",
   "usageTips.tip5.title": "技巧5-解除標籤:",
-  "usageTips.tip5.detail": "長按應用程式圖示開始拖曳 > 把應用拖到容器之間的空白處",
+  "usageTips.tip5.detail": "長按應用程式圖示開始拖曳 > 拖到容器之間的空白處",
   "usageTips.tip6.title": "技巧6-標籤排序:",
   "usageTips.tip6.detail": "長按標籤開始拖曳 > 拖到目標位置後放開",
   "usageTips.tip7.title": "技巧7-應用排序:",
diff --git a/src/CHANGELOG.md b/src/CHANGELOG.md
index b296679..83a1e1c 100644
--- a/src/CHANGELOG.md
+++ b/src/CHANGELOG.md
@@ -1,5 +1,13 @@
 # TagLauncher Changelog
 
+## [7.9.1] — 2026-06-13
+
+- 修复新建空标签后 App Grid 左侧标签栏和主体区域可能不显示该标签的问题;空标签现在会按当前视图显示最小容器,避免用户以为创建失败
+- 修复拖动应用到容器外空白处解除标签时,部分 macOS 14 环境可能没有反应的问题;容器内部空白仍不会触发解除标签,保持产品口径一致
+- App Grid 新增原生 AppKit 底部使用技巧悬浮条,支持翻页、可在设置中关闭、不会穿透点击到底层应用,并为 29 个语种的较长文案预留换行展示能力
+- 优化使用技巧悬浮条排版:标题完整展示,正文分隔符按换行处理,页签指示点移动到左右箭头下方,右侧控制区固定,减少遮挡和误触
+- 版本号更新为 `7.9.1`,Build 更新为 `20260613.2020`
+
 ## [7.9.0] — 2026-06-11
 
 - 新增容器内 App 手动拖拽排序:同一容器内拖动 App 可改变显示顺序,不改变 App 与标签的归属关系
diff --git a/src/Scripts/usage_tips_qa.sh b/src/Scripts/usage_tips_qa.sh
index 78834f8..3bc4a2a 100755
--- a/src/Scripts/usage_tips_qa.sh
+++ b/src/Scripts/usage_tips_qa.sh
@@ -86,9 +86,9 @@
     "Usage tips must be implemented as a native AppKit NSView",
 )
 require(
-    r"enum\s+AppGridUsageTipsMetrics\s*\{(?P<body>.*?)static\s+let\s+reservedHeight\s*:\s*CGFloat\s*=\s*128",
+    r"enum\s+AppGridUsageTipsMetrics\s*\{(?P<body>.*?)static\s+let\s+barHeight\s*:\s*CGFloat\s*=\s*136(?P<body2>.*?)static\s+let\s+reservedHeight\s*:\s*CGFloat\s*=\s*176",
     app_grid,
-    "Usage tips overlay must reserve stable bottom space for the native HUD bar",
+    "Usage tips overlay must reserve a design-reviewed multi-line bottom space for the native HUD bar",
 )
 require(
     r"NSVisualEffectView\s*\(",
@@ -125,19 +125,34 @@
     "Usage tips must ask the host to recompute width after localized text changes",
 )
 require(
-    r"availableTextWidth\s*=\s*max\s*\(\s*1\s*,\s*visualFrame\.maxX\s*-\s*titleX\s*-\s*titleDetailGap\s*-\s*trailingWidth\s*\)",
+    r"controlZoneWidth\s*:\s*CGFloat\s*=\s*176",
+    app_grid,
+    "Usage tips layout must reserve a fixed right-side control zone",
+)
+require(
+    r"availableTextWidth\s*=\s*max\s*\(\s*1\s*,\s*textRight\s*-\s*titleX\s*\)",
     app_grid,
     "Usage tips layout must allocate text from actual available AppGrid width",
 )
 require(
-    r"dotsView\.frame\s*=\s*NSRect\s*\((?P<body>.*?)x\s*:\s*buttonsGroupMinX\s*\+\s*\(buttonsWidth\s*-\s*dotsWidth\)\s*/\s*2(?P<body2>.*?)y\s*:\s*previousButton\.frame\.maxY\s*\+\s*6",
+    r"dotsView\.frame\s*=\s*NSRect\s*\((?P<body>.*?)x\s*:\s*buttonsGroupMinX\s*\+\s*\(buttonsWidth\s*-\s*dotsWidth\)\s*/\s*2(?P<body2>.*?)y\s*:\s*previousButton\.frame\.maxY\s*\+\s*buttonDotsGap",
     app_grid,
     "Usage tips page dots must sit centered below the previous/next arrow buttons",
 )
 require(
-    r"detailRight\s*=\s*previousButton\.frame\.minX\s*-\s*detailControlsGap",
+    r"detailRight\s*=\s*textRight",
     app_grid,
-    "Usage tips detail text must stop before the arrow controls when dots move below them",
+    "Usage tips detail text must stop before the fixed right-side arrow control zone",
+)
+require(
+    r"configureDetailLabel\s*\(\s*\)(?P<body>.*?)detailLabel\.lineBreakMode\s*=\s*\.byWordWrapping",
+    app_grid,
+    "Usage tip detail text must wrap naturally inside the reserved text column",
+)
+require(
+    r"detailLabel\.maximumNumberOfLines\s*=\s*2",
+    app_grid,
+    "Usage tip detail text must use the design-reviewed two-line body layout",
 )
 require(
     r"return\s+min\s*\(\s*maxAvailableWidth\s*,\s*preferredWidth\s*\)",
@@ -346,7 +361,12 @@
 require(
     r"formattedTipDetail\s*\(_\s+text\s*:\s*String\s*\)",
     app_grid,
-    "Usage tips detail text must normalize > separators to -> for display",
+    "Usage tips detail text must normalize separators for display",
+)
+require(
+    r"replacingOccurrences\s*\(\s*of\s*:\s*\"\\\\s\*\(\?:-->\|->\|>\|→\|>\)\\\\s\*\"\s*,\s*with\s*:\s*\"\\n\"",
+    app_grid,
+    "Usage tips detail text must convert -->, ->, and > separators into hard line breaks",
 )
 require(
     r"setUsageTipsBubbleDisabled\s*\(\s*inside\s*\)",

--
Gitblit v1.9.3