Rework .agents/, Claude/Codex config, CI workflows, lint gates and lint-rule tests, and the release/changelog tooling. The 5% comment density gate stays diff-scoped inside git hooks, where GIT_DIR outranks the -C the gate uses to locate the repository. The tray icon generator writes multi-size ICO files from the SVG sources into the per-platform tray asset directories. The custom code-review subagents are gone: the built-in /code-review never dispatched them, and the subagent model now lives in a user-level setting outside the repository. The Makefile, build_config.yaml and CI hand the Core and Helper builds to the setup build hook, so the buildkit launchers go, and setup.dart packages AppImage and rpm on arm64 with flutter_distributor pinned to the fork tag that normalizes packaging permissions. Changelog: skip
6.6 KiB
name, description
| name | description |
|---|---|
| ui-work | Use when changing FlClash Flutter UI, widgets, screens, Material You styling, navigation surfaces, async feedback, or user-facing interactions. |
UI Work
When To Use
Use this for user-facing Flutter UI changes in lib/, including widgets, screens, navigation surfaces, settings rows, dialogs, and interaction behavior.
Workflow
-
Locate existing nearby widgets and reuse their patterns before adding new abstractions.
-
Follow current Material You and Surfboard-like visual conventions.
-
Use existing providers, notifiers, and helpers where possible.
-
Keep
child:last in widget constructors. -
Prefer
constconstructors and final locals. -
Localize user-facing text through ARB; use
localizationwhen text changes are non-trivial. -
Add focused widget tests when behavior changes, especially for rendering states, taps, scrolling, and empty/error states.
-
For asynchronous controls, define separately:
- authoritative provider/domain state;
- display-only state such as a minimum progress duration;
- tap policy while work or display holds are active;
- failure/disposal cleanup, normally in
finallyfor animations and timers.
-
Run targeted verification:
flutter analyze flutter test test/widgets/
Corner Radii
All corner radii come from lib/common/shape.dart. Never write a radius literal in a widget.
The scale is picked by the component's shortest side, not by what looks good in isolation. A radius that reads as a soft card at 64 logical pixels tall reads as a pill at 24 and as a square at 400, so a single radius everywhere is the wrong kind of consistency.
| Token | Value | Shortest side | Use |
|---|---|---|---|
none |
0 | - | square edges, and the flat side of a grouped run |
xs |
4 | inset blocks | tiles clipped inside an already rounded surface |
sm |
8 | up to 48 | chips, thumbs, swatches, small square tiles |
md |
16 | up to 64 | interactive chrome: inputs, menus, buttons, popups, FAB |
lg |
20 | 64 to 180 | mid-size cards: dashboard tiles, proxy node cards |
xl |
24 | full-bleed | anything spanning the whole width: list rows, grouped runs, group headers |
xxl |
28 | over 200 | sheets, dialogs, full-screen containers |
full |
1000 | - | pills and circles: tracks, indicators, progress, avatars |
AppCorner.fit(shortestSide) applies that table at runtime and is the right call whenever the size comes from a
LayoutBuilder or scales with text size. It snaps to the largest token that stays at or under one third of the
shortest side, which is the rule the table encodes.
AppCornerholds the scale asdouble, forradius:onCommonCardand for arithmetic.AppRadiusmirrors it asBorderRadius, plusall,top, andverticalbuilders.AppShapemirrors it asRoundedSuperellipseBorder, plusfull(stadium),circle,input, and theall/top/vertical/ofbuilders.ThemeData.withAppShapesinlib/application.dartapplies the scale to card, dialog, bottom sheet, snack bar, chip, menu, input, FAB, navigation indicator, and progress themes. Do not restate those shapes at call sites; in particular, leaveInputDecoration.borderunset so inputs inheritAppShape.input.
The three tiers between md and xxl each answer to a different size class, and the split is what keeps one radius
from being wrong for two of them.
xl is for a surface that spans the full width. A wide, short strip needs a larger corner than its height alone
suggests, or it stops reading as a card. Everything full-bleed shares this token and must stay on it:
CommonSelectedListItem, the profiles card, the outer corners of a grouped run in DecorationListItem
(generateSectionV3), and the proxy group header in lib/views/proxies/list.dart. The value is deliberately under
half the height of a standard row, so nothing gets clamped and every one of them renders at the identical radius no
matter which is taller. This is the one place the one-third rule is knowingly overshot; the width is what carries
it, and fit() is not used here.
lg is for the mid-size card that is not full-bleed: dashboard tiles and proxy node cards. It exists because xl
does not fit all of them. A proxy node card's height follows the user's ProxyCardType setting, and at min it is
only about 64 tall, which caps its radius at 21 — so xl would break on a setting the user can change at any time,
while lg clears every one of the three heights. The same value has room to spare on the smallest dashboard tile,
about 177x80, so both surfaces can hold one token instead of branching per size.
Outlined inputs use AppInputBorder, not ShapedInputBorder from package:material_ui. That one subtracts the
floating label's notch from the outline as a two-pixel band along the top edge, which assumes the label sits over a
flat run of border. Any radius from 8 up puts the corner curve under the notch instead, the subtraction takes the
corner with it, and the top edge is left drawn a pixel low. AppInputBorder clips the notch region away and paints
the full superellipse through it, so the corner arc is truncated exactly where the notch begins. That is what
Flutter's own OutlineInputBorder does by shortening the corner arc's sweep, which is why the framework border
survives large radii and the package one does not. The border needs no contentPadding compensation; leave Material's
defaults alone.
Nested radii are derived, never tokens. Concentric corners need outer = inner + inset, so name the inset and
compute the outer value: _cardRadius in lib/widgets/popup.dart, _kCornerRadius in lib/widgets/tab.dart, and
the selection ring in lib/widgets/palette.dart all do this. Adding an intermediate token to spell one of these out
is what makes a scale grow without bound.
Pitfalls
- Do not introduce a new visual system for one screen.
- Do not manually edit generated localization or provider files.
- Avoid broad layout rewrites unless the requested change requires them.
- Do not mutate provider/domain state merely to smooth a transition. Keep presentation holds local and let real errors bypass them immediately.
- Do not leave loading animations active when callbacks throw. Test the exception path, not only the successful tap.
Current Interaction Examples
CoreStatusButtonwatchescoreStatusProviderbut keeps its 600-millisecond connecting hold locally. Taps are ignored during the hold or genuine connecting state; disconnected cancels the hold immediately.- Proxy delay testing writes
0while pending, the measured delay on success, and-1on failure.DelayTestButtonresets its animation infinally.