// widgets/ds/ds_atoms.dart // Flutter 组件库(ds=design-system)——一对一镜像原型 atoms.css 的原子。 // 单一组件源:尺寸/间距/圆角/字号全引 AppDims,颜色全引 context.tokens(禁硬编码)。 // 屏只组合这些组件,不再自己堆样式(见代码端真相源闸 check_ds_code)。 import 'package:flutter/material.dart'; import '../../core/theme/context_tokens.dart'; import '../../core/theme/app_dims.g.dart'; // ── .btn ────────────────────────────────────────────────────────────────── enum DsBtnVariant { ghost, primary, danger } /// 原型 .btn:h38 / pad 0 16 / r-md / fs-body / fw600 / gap7,图标 16。 /// ghost=surface+border / primary=primary+on-primary / danger=danger+白。 class DsButton extends StatelessWidget { final String label; final IconData? icon; final VoidCallback? onPressed; final DsBtnVariant variant; final bool small; // .btn.sm:h32 / pad 0 12 / fs-sm const DsButton( this.label, { super.key, this.icon, this.onPressed, this.variant = DsBtnVariant.ghost, this.small = false, }); @override Widget build(BuildContext context) { final t = context.tokens; final (bg, fg, border) = switch (variant) { DsBtnVariant.ghost => (t.surface, t.text, t.border), DsBtnVariant.primary => (t.primary, t.onPrimary, t.primary), DsBtnVariant.danger => (t.danger, t.onPrimary, t.danger), }; return Material( color: bg, borderRadius: BorderRadius.circular(AppDims.rMd), child: InkWell( onTap: onPressed, borderRadius: BorderRadius.circular(AppDims.rMd), child: Container( height: small ? 32 : 38, padding: EdgeInsets.symmetric(horizontal: small ? 12 : 16), decoration: BoxDecoration( borderRadius: BorderRadius.circular(AppDims.rMd), border: Border.all(color: border), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ if (icon != null) ...[ Icon(icon, size: 16, color: fg), const SizedBox(width: 7), ], Text(label, style: TextStyle( color: fg, fontSize: small ? AppDims.fsSm : AppDims.fsBody, fontWeight: FontWeight.w600)), ], ), ), ), ); } } // ── .badge ──────────────────────────────────────────────────────────────── enum DsBadgeTone { ok, danger, warn, info, accent, muted } /// 原型 .badge:h22 / pad 0 9 / r-pill / fs-sm fw600 + 前导圆点(6px, currentColor)。 class DsBadge extends StatelessWidget { final String label; final DsBadgeTone tone; const DsBadge(this.label, {super.key, this.tone = DsBadgeTone.ok}); @override Widget build(BuildContext context) { final t = context.tokens; final (bg, fg) = switch (tone) { DsBadgeTone.ok => (t.successBg, t.success), DsBadgeTone.danger => (t.dangerBg, t.danger), DsBadgeTone.warn => (t.warnBg, t.warn), DsBadgeTone.info => (t.infoSoft, t.primary), DsBadgeTone.accent => (t.accentSoft, t.accent), DsBadgeTone.muted => (t.borderSubtle, t.muted), }; return Container( height: 22, padding: const EdgeInsets.symmetric(horizontal: 9), decoration: BoxDecoration(color: bg, borderRadius: BorderRadius.circular(AppDims.rPill)), child: Row( mainAxisSize: MainAxisSize.min, children: [ Container( width: 6, height: 6, decoration: BoxDecoration(color: fg, shape: BoxShape.circle)), const SizedBox(width: 5), Text(label, style: TextStyle( color: fg, fontSize: AppDims.fsSm, fontWeight: FontWeight.w600)), ], ), ); } } // ── .chip ───────────────────────────────────────────────────────────────── /// 原型 .chip:h34 / pad 0 12 / border r-md / surface / fs-sm / gap7。 /// 选中值用 .cv(primary fw600);激活(on)边框 primary。配 PopupMenuButton 用。 class DsChip extends StatelessWidget { final String label; final String? value; // 选中值(.cv);非空即 on 态 final VoidCallback? onTap; const DsChip({super.key, required this.label, this.value, this.onTap}); @override Widget build(BuildContext context) { final t = context.tokens; final on = value != null && value!.isNotEmpty; return InkWell( onTap: onTap, borderRadius: BorderRadius.circular(AppDims.rMd), child: Container( height: 34, padding: const EdgeInsets.symmetric(horizontal: 12), decoration: BoxDecoration( color: t.surface, border: Border.all(color: on ? t.primary : t.border), borderRadius: BorderRadius.circular(AppDims.rMd), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Text(label, style: TextStyle(fontSize: AppDims.fsSm, color: t.text)), if (on) ...[ const SizedBox(width: 7), Text(value!, style: TextStyle( fontSize: AppDims.fsSm, color: t.primary, fontWeight: FontWeight.w600)), ], const SizedBox(width: 7), Icon(Icons.keyboard_arrow_down, size: 14, color: t.faint), ], ), ), ); } } // ── .searchbox ────────────────────────────────────────────────────────────── /// 原型 .searchbox:h34 / bg / border r-md / gap8 / pad 0 11,前导放大镜 14(faint)。 class DsSearchBox extends StatelessWidget { final TextEditingController? controller; final String hint; final ValueChanged? onChanged; final ValueChanged? onSubmitted; final double? width; const DsSearchBox({ super.key, this.controller, this.hint = '', this.onChanged, this.onSubmitted, this.width, }); @override Widget build(BuildContext context) { final t = context.tokens; return SizedBox( width: width, height: 34, child: Container( padding: const EdgeInsets.symmetric(horizontal: 11), decoration: BoxDecoration( color: t.bg, border: Border.all(color: t.border), borderRadius: BorderRadius.circular(AppDims.rMd), ), child: Row( children: [ Icon(Icons.search, size: 14, color: t.faint), const SizedBox(width: 8), Expanded( child: TextField( controller: controller, onChanged: onChanged, onSubmitted: onSubmitted, style: TextStyle(fontSize: AppDims.fsBody, color: t.text), decoration: InputDecoration( isCollapsed: true, border: InputBorder.none, hintText: hint, hintStyle: TextStyle(fontSize: AppDims.fsBody, color: t.faint), ), ), ), ], ), ), ); } }