feat(client): tablet 外壳 + 视觉 diff 工作流文档

- tablet_shell: 侧栏 232·4 项触控(minHeight 48, 字号 15) + 顶栏无主题切换 + 连接页双栏
  对照 ui_kits/tablet/tabapp.jsx;home_shell 分发 tablet→TabletShell
- nav_sidebar 加 dense 参数(desktop 紧凑 / tablet 触控)
- content_top_bar 加 showThemeToggle + titleSize 参数
- tool/visual-diff.md: 截图 diff 工作流(design-distill 工具)+ 实现侧三路径
  现状(golden 被 google_fonts 阻塞 / screencapture 受 macOS chrome 限制)+ 推荐

注: tablet 形态在 macOS 桌面平台不触发(只 desktop/mobile),运行验证需 iPad。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
wangjia
2026-06-16 09:38:18 +08:00
parent 4dc4127252
commit 281a6261fb
5 changed files with 159 additions and 31 deletions
+15 -11
View File
@@ -12,10 +12,12 @@ import '../state/nodes_provider.dart';
import 'pangolin_icons.dart';
class ContentTopBar extends ConsumerWidget {
const ContentTopBar({super.key, required this.title, this.onBack});
const ContentTopBar({super.key, required this.title, this.onBack, this.showThemeToggle = true, this.titleSize = 17});
final String title;
final VoidCallback? onBack;
final bool showThemeToggle;
final double titleSize;
@override
Widget build(BuildContext context, WidgetRef ref) {
@@ -40,7 +42,7 @@ class ContentTopBar extends ConsumerWidget {
const SizedBox(width: 4),
],
Text(title,
style: PangolinText.h3.copyWith(color: c.fg1, fontWeight: FontWeight.w700, fontSize: 17)),
style: PangolinText.h3.copyWith(color: c.fg1, fontWeight: FontWeight.w700, fontSize: titleSize)),
const Spacer(),
Text(
conn.phase == VpnPhase.on ? '${node.code}' : '',
@@ -50,16 +52,18 @@ class ContentTopBar extends ConsumerWidget {
color: conn.phase == VpnPhase.on ? c.success : c.fg3,
),
),
const SizedBox(width: 12),
InkWell(
onTap: () => ref.read(themeModeProvider.notifier).state =
isDark ? ThemeMode.light : ThemeMode.dark,
borderRadius: BorderRadius.circular(PangolinRadius.sm),
child: Padding(
padding: const EdgeInsets.all(6),
child: Icon(isDark ? PangolinIcons.sun : PangolinIcons.moon, size: 18, color: c.fg2),
if (showThemeToggle) ...[
const SizedBox(width: 12),
InkWell(
onTap: () => ref.read(themeModeProvider.notifier).state =
isDark ? ThemeMode.light : ThemeMode.dark,
borderRadius: BorderRadius.circular(PangolinRadius.sm),
child: Padding(
padding: const EdgeInsets.all(6),
child: Icon(isDark ? PangolinIcons.sun : PangolinIcons.moon, size: 18, color: c.fg2),
),
),
),
],
]),
);
}
+28 -19
View File
@@ -15,11 +15,14 @@ import 'plan_badge_card.dart';
typedef NavSidebarItem = ({IconData icon, String label, NavView view});
class NavSidebar extends ConsumerWidget {
const NavSidebar({super.key, required this.items, this.width = 204});
const NavSidebar({super.key, required this.items, this.width = 204, this.dense = true});
final List<NavSidebarItem> items;
final double width;
/// dense=true 桌面紧凑(行高~36)dense=false 平板触控(行高≥48, 字号更大)。
final bool dense;
@override
Widget build(BuildContext context, WidgetRef ref) {
final c = context.pangolin;
@@ -32,17 +35,18 @@ class NavSidebar extends ConsumerWidget {
color: c.bgSubtle,
border: Border(right: BorderSide(color: c.border)),
),
padding: const EdgeInsets.fromLTRB(12, 14, 12, 14),
padding: EdgeInsets.fromLTRB(dense ? 12 : 14, dense ? 14 : 18, dense ? 12 : 14, dense ? 14 : 16),
child: Column(crossAxisAlignment: CrossAxisAlignment.stretch, children: [
// 品牌区
Padding(
padding: const EdgeInsets.fromLTRB(6, 2, 6, 16),
padding: EdgeInsets.fromLTRB(6, 2, 6, dense ? 16 : 20),
child: Row(children: [
const PangolinMark(size: 28),
const SizedBox(width: 9),
PangolinMark(size: dense ? 28 : 30),
SizedBox(width: dense ? 9 : 10),
Column(crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [
Text(t.brand,
style: PangolinText.h3.copyWith(color: c.fg1, fontWeight: FontWeight.w700, fontSize: 16, height: 1)),
style: PangolinText.h3.copyWith(
color: c.fg1, fontWeight: FontWeight.w700, fontSize: dense ? 16 : 17, height: 1)),
const SizedBox(height: 3),
Text('PANGOLIN',
style: PangolinText.caption.copyWith(
@@ -53,11 +57,12 @@ class NavSidebar extends ConsumerWidget {
// Nav 列表
for (final item in items)
Padding(
padding: const EdgeInsets.only(bottom: 3),
padding: EdgeInsets.only(bottom: dense ? 3 : 4),
child: _NavItem(
icon: item.icon,
label: item.label,
active: current == item.view,
dense: dense,
onTap: () => ref.read(navViewProvider.notifier).state = item.view,
),
),
@@ -69,11 +74,12 @@ class NavSidebar extends ConsumerWidget {
}
class _NavItem extends StatelessWidget {
const _NavItem({required this.icon, required this.label, required this.active, required this.onTap});
const _NavItem({required this.icon, required this.label, required this.active, required this.onTap, this.dense = true});
final IconData icon;
final String label;
final bool active;
final VoidCallback onTap;
final bool dense;
@override
Widget build(BuildContext context) {
@@ -84,17 +90,20 @@ class _NavItem extends StatelessWidget {
clipBehavior: Clip.antiAlias,
child: InkWell(
onTap: onTap,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 9),
child: Row(children: [
Icon(icon, size: 18, color: active ? c.accent : c.fg3),
const SizedBox(width: 11),
Text(label,
style: PangolinText.sm.copyWith(
color: active ? c.accent : c.fg2,
fontSize: 13.5,
fontWeight: active ? FontWeight.w600 : FontWeight.w500)),
]),
child: ConstrainedBox(
constraints: BoxConstraints(minHeight: dense ? 0 : 48),
child: Padding(
padding: EdgeInsets.symmetric(horizontal: dense ? 12 : 14, vertical: dense ? 9 : 13),
child: Row(children: [
Icon(icon, size: dense ? 18 : 20, color: active ? c.accent : c.fg3),
SizedBox(width: dense ? 11 : 12),
Text(label,
style: PangolinText.sm.copyWith(
color: active ? c.accent : c.fg2,
fontSize: dense ? 13.5 : 15,
fontWeight: active ? (dense ? FontWeight.w600 : FontWeight.w700) : FontWeight.w500)),
]),
),
),
),
);