fix(studio): store overflow + visual clutter (v0.31.1)
Two issues from the user feedback after v0.31.0:
1. RenderFlex overflow in the store body. The inner Column
wrapped the editorial chrome (Today, Recommended,
Featured) plus an Expanded grid; on small windows the
chrome alone exceeded the available height and the grid
got squeezed past zero, painting the yellow-and-black
stripe.
Fix: wrap the inner Column in a SingleChildScrollView so
chrome and grid scroll as one continuous surface
(App-Store / Play-Store behaviour). The grid switches to
`shrinkWrap: true` + `NeverScrollableScrollPhysics()` so
it doesn't try to claim its own viewport.
2. "Der Store ist zu unaufgeräumt, ich fühle mich
erschlagen." — three editorial cards stacked above the
filter row + grid was too much.
Visual cleanup:
- The recommended-source quick-add chips are now inlined
into the Today hero's footer (same row as the CTA).
The standalone `_RecommendedSourcesStrip` only fires in
the corner case where Today is dismissed AND no
federation exists yet.
- The Featured strip no longer competes with Today —
it only renders once Today has been dismissed. Today
plays the editorial-hero role; Featured is the
fallback editorial surface.
- Provenance pill ("native") is dropped on native cards.
Native is the default and doesn't earn a badge — only
foreign code (mcp / n8n) gets the provenance pill, the
same way "verified" is opt-in in commercial app stores.
Net: at most one editorial card above the grid at any time.
Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
This commit is contained in:
parent
ce97300a12
commit
b0f09260e1
3 changed files with 121 additions and 80 deletions
|
|
@ -26,7 +26,7 @@ import 'widgets/widgets.dart';
|
||||||
/// Studio's own build version. Bump on every UI commit so the
|
/// Studio's own build version. Bump on every UI commit so the
|
||||||
/// running app self-identifies — visible in the sidebar header
|
/// running app self-identifies — visible in the sidebar header
|
||||||
/// and quick-glance proof that you're seeing the current build.
|
/// and quick-glance proof that you're seeing the current build.
|
||||||
const String kStudioVersion = '0.31.0';
|
const String kStudioVersion = '0.31.1';
|
||||||
|
|
||||||
Future<void> main() async {
|
Future<void> main() async {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
|
||||||
|
|
@ -232,41 +232,34 @@ class _StorePageState extends State<StorePage> {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// Featured strip is only useful in the
|
// Browsing-only chrome — every editorial
|
||||||
// browse-the-whole-thing case. Once the
|
// surface hides the moment a filter is set so
|
||||||
// operator has typed a query or picked a
|
// the operator's query stays the focus.
|
||||||
// filter, the result list itself is the
|
final isBrowsing = _queryCtrl.text.trim().isEmpty &&
|
||||||
// answer they want — featured chrome would
|
|
||||||
// just push it down.
|
|
||||||
final showFeatured = _queryCtrl.text.trim().isEmpty &&
|
|
||||||
_category.isEmpty &&
|
|
||||||
_status.isEmpty &&
|
|
||||||
_source.isEmpty &&
|
|
||||||
!_installedOnly &&
|
|
||||||
items.any((e) => e.featured);
|
|
||||||
// Sparse-store nudge: when no filter is set
|
|
||||||
// and the store carries no federated entries
|
|
||||||
// yet, surface curated public MCP servers as
|
|
||||||
// one-click add cards. Hides the moment any
|
|
||||||
// federated entry shows up — the strip has
|
|
||||||
// done its job.
|
|
||||||
final showRecommendedStrip =
|
|
||||||
_queryCtrl.text.trim().isEmpty &&
|
|
||||||
_category.isEmpty &&
|
|
||||||
_status.isEmpty &&
|
|
||||||
_source.isEmpty &&
|
|
||||||
!_installedOnly &&
|
|
||||||
!raw.any((e) => e.isFederated);
|
|
||||||
// Editorial hero is browsing-only chrome —
|
|
||||||
// hides as soon as any filter is set so the
|
|
||||||
// operator's actual query stays in focus.
|
|
||||||
final showToday = !_todayDismissed &&
|
|
||||||
_queryCtrl.text.trim().isEmpty &&
|
|
||||||
_category.isEmpty &&
|
_category.isEmpty &&
|
||||||
_status.isEmpty &&
|
_status.isEmpty &&
|
||||||
_source.isEmpty &&
|
_source.isEmpty &&
|
||||||
!_installedOnly;
|
!_installedOnly;
|
||||||
return Column(
|
final hasNoFederation = !raw.any((e) => e.isFederated);
|
||||||
|
final showToday = !_todayDismissed && isBrowsing;
|
||||||
|
// Recommended-source quick-adds are inlined in
|
||||||
|
// the Today footer when both apply, so the
|
||||||
|
// operator never sees two competing editorial
|
||||||
|
// cards stacked on top of each other.
|
||||||
|
final embedRecommended = showToday && hasNoFederation;
|
||||||
|
// Standalone strip only fires in the corner
|
||||||
|
// case: Today dismissed AND no federation.
|
||||||
|
// Featured strip and Today never compete —
|
||||||
|
// Today is the editorial hero, Featured plays
|
||||||
|
// second fiddle and only renders once Today is
|
||||||
|
// out of the way.
|
||||||
|
final showStandaloneRecommended =
|
||||||
|
isBrowsing && _todayDismissed && hasNoFederation;
|
||||||
|
final showFeatured = isBrowsing &&
|
||||||
|
_todayDismissed &&
|
||||||
|
items.any((e) => e.featured);
|
||||||
|
return SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
if (showToday) ...[
|
if (showToday) ...[
|
||||||
|
|
@ -278,16 +271,21 @@ class _StorePageState extends State<StorePage> {
|
||||||
onCta: _todayStory.cta == TodayCta.openSettings
|
onCta: _todayStory.cta == TodayCta.openSettings
|
||||||
? () => FaiSettingsDialog.show(context)
|
? () => FaiSettingsDialog.show(context)
|
||||||
: null,
|
: null,
|
||||||
|
recommendedSources: embedRecommended
|
||||||
|
? _kRecommendedSources
|
||||||
|
: const <_RecommendedSource>[],
|
||||||
|
recommendedAdding: _addingSources,
|
||||||
|
onAddRecommended: _addRecommendedSource,
|
||||||
),
|
),
|
||||||
const SizedBox(height: FaiSpace.md),
|
const SizedBox(height: FaiSpace.lg),
|
||||||
],
|
],
|
||||||
if (showRecommendedStrip) ...[
|
if (showStandaloneRecommended) ...[
|
||||||
_RecommendedSourcesStrip(
|
_RecommendedSourcesStrip(
|
||||||
adding: _addingSources,
|
adding: _addingSources,
|
||||||
locale: _locale,
|
locale: _locale,
|
||||||
onAdd: _addRecommendedSource,
|
onAdd: _addRecommendedSource,
|
||||||
),
|
),
|
||||||
const SizedBox(height: FaiSpace.md),
|
const SizedBox(height: FaiSpace.lg),
|
||||||
],
|
],
|
||||||
if (showFeatured) ...[
|
if (showFeatured) ...[
|
||||||
_FeaturedStrip(
|
_FeaturedStrip(
|
||||||
|
|
@ -298,16 +296,15 @@ class _StorePageState extends State<StorePage> {
|
||||||
),
|
),
|
||||||
const SizedBox(height: FaiSpace.lg),
|
const SizedBox(height: FaiSpace.lg),
|
||||||
],
|
],
|
||||||
Expanded(
|
_StoreGrid(
|
||||||
child: _StoreGrid(
|
|
||||||
items: items,
|
items: items,
|
||||||
locale: _locale,
|
locale: _locale,
|
||||||
installedVersions: _installedVersions,
|
installedVersions: _installedVersions,
|
||||||
onTap: _openDetail,
|
onTap: _openDetail,
|
||||||
onInstall: _install,
|
onInstall: _install,
|
||||||
),
|
),
|
||||||
),
|
|
||||||
],
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
@ -603,8 +600,16 @@ class _StoreGrid extends StatelessWidget {
|
||||||
builder: (context, constraints) {
|
builder: (context, constraints) {
|
||||||
const minCardWidth = 360.0;
|
const minCardWidth = 360.0;
|
||||||
final cols = (constraints.maxWidth / minCardWidth).floor().clamp(1, 4);
|
final cols = (constraints.maxWidth / minCardWidth).floor().clamp(1, 4);
|
||||||
|
// shrinkWrap + NeverScrollable lets the grid sit inside
|
||||||
|
// the page-level SingleChildScrollView so editorial
|
||||||
|
// chrome and the grid scroll as one continuous surface
|
||||||
|
// (App-Store / Play-Store behaviour). Without this, the
|
||||||
|
// inner grid claims its own scroll viewport and the
|
||||||
|
// outer Column overflows on small windows.
|
||||||
return GridView.builder(
|
return GridView.builder(
|
||||||
padding: EdgeInsets.zero,
|
padding: EdgeInsets.zero,
|
||||||
|
shrinkWrap: true,
|
||||||
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||||
crossAxisCount: cols,
|
crossAxisCount: cols,
|
||||||
mainAxisSpacing: FaiSpace.md,
|
mainAxisSpacing: FaiSpace.md,
|
||||||
|
|
@ -1972,12 +1977,23 @@ class _StoreTodayHero extends StatelessWidget {
|
||||||
/// because navigation targets live in the store-page state,
|
/// because navigation targets live in the store-page state,
|
||||||
/// not in const story data.
|
/// not in const story data.
|
||||||
final VoidCallback? onCta;
|
final VoidCallback? onCta;
|
||||||
|
/// Inline one-click "+ Add public source" chips rendered in
|
||||||
|
/// the hero footer. Empty list hides the row — the parent
|
||||||
|
/// passes [_kRecommendedSources] only when the store has zero
|
||||||
|
/// federated entries, so we never duplicate the standalone
|
||||||
|
/// strip.
|
||||||
|
final List<_RecommendedSource> recommendedSources;
|
||||||
|
final Set<String> recommendedAdding;
|
||||||
|
final void Function(_RecommendedSource)? onAddRecommended;
|
||||||
|
|
||||||
const _StoreTodayHero({
|
const _StoreTodayHero({
|
||||||
required this.story,
|
required this.story,
|
||||||
required this.locale,
|
required this.locale,
|
||||||
required this.onDismiss,
|
required this.onDismiss,
|
||||||
required this.onCta,
|
required this.onCta,
|
||||||
|
this.recommendedSources = const <_RecommendedSource>[],
|
||||||
|
this.recommendedAdding = const <String>{},
|
||||||
|
this.onAddRecommended,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -2082,8 +2098,14 @@ class _StoreTodayHero extends StatelessWidget {
|
||||||
height: 1.45,
|
height: 1.45,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (onCta != null) ...[
|
if (onCta != null || recommendedSources.isNotEmpty) ...[
|
||||||
const SizedBox(height: FaiSpace.md),
|
const SizedBox(height: FaiSpace.md),
|
||||||
|
Wrap(
|
||||||
|
spacing: FaiSpace.sm,
|
||||||
|
runSpacing: FaiSpace.xs,
|
||||||
|
crossAxisAlignment: WrapCrossAlignment.center,
|
||||||
|
children: [
|
||||||
|
if (onCta != null)
|
||||||
FilledButton.tonalIcon(
|
FilledButton.tonalIcon(
|
||||||
onPressed: onCta,
|
onPressed: onCta,
|
||||||
icon: const Icon(Icons.arrow_forward, size: 14),
|
icon: const Icon(Icons.arrow_forward, size: 14),
|
||||||
|
|
@ -2092,6 +2114,24 @@ class _StoreTodayHero extends StatelessWidget {
|
||||||
visualDensity: VisualDensity.compact,
|
visualDensity: VisualDensity.compact,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
for (final s in recommendedSources)
|
||||||
|
ActionChip(
|
||||||
|
avatar: recommendedAdding.contains(s.name)
|
||||||
|
? const SizedBox(
|
||||||
|
width: 12,
|
||||||
|
height: 12,
|
||||||
|
child:
|
||||||
|
CircularProgressIndicator(strokeWidth: 2),
|
||||||
|
)
|
||||||
|
: Icon(s.icon, size: 14),
|
||||||
|
label: Text('+ ${s.label}'),
|
||||||
|
onPressed:
|
||||||
|
recommendedAdding.contains(s.name) || onAddRecommended == null
|
||||||
|
? null
|
||||||
|
: () => onAddRecommended!(s),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
@ -2340,10 +2380,11 @@ class _ProvenancePill extends StatelessWidget {
|
||||||
icon: Icons.account_tree_outlined,
|
icon: Icons.account_tree_outlined,
|
||||||
);
|
);
|
||||||
default:
|
default:
|
||||||
return FaiPill(
|
// Native is the default: no badge needed. Keeps the
|
||||||
label: l.storeProvenanceNative,
|
// card visually quiet — only foreign code (mcp / n8n)
|
||||||
tone: FaiPillTone.neutral,
|
// earns the provenance pill, the same way a "verified"
|
||||||
);
|
// badge is opt-in in commercial app stores.
|
||||||
|
return const SizedBox.shrink();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
name: fai_studio
|
name: fai_studio
|
||||||
description: "F∆I Studio — desktop GUI for the F∆I hub"
|
description: "F∆I Studio — desktop GUI for the F∆I hub"
|
||||||
publish_to: 'none'
|
publish_to: 'none'
|
||||||
version: 0.31.0
|
version: 0.31.1
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.11.0-200.1.beta
|
sdk: ^3.11.0-200.1.beta
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue