feat(studio): award-tier visual polish
Brightness-aware elevation tokens (FaiElevation low/medium) applied to cards, sheets and the sidebar active item; gentle hover-lift on Store + Doc cards; a longer, eased sidebar expand using the under-used FaiMotion.slow; and a branded empty state carrying the F∆I delta mark with warmer spacing. Subtle, Linear/Raycast-style — both light and dark themes tuned. Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
This commit is contained in:
parent
5313266cc4
commit
f0a944dce7
4 changed files with 261 additions and 146 deletions
|
|
@ -1792,7 +1792,7 @@ class _FeaturedTile extends StatelessWidget {
|
|||
}
|
||||
}
|
||||
|
||||
class _StoreCard extends StatelessWidget {
|
||||
class _StoreCard extends StatefulWidget {
|
||||
final StoreItem item;
|
||||
final String locale;
|
||||
|
||||
|
|
@ -1810,6 +1810,19 @@ class _StoreCard extends StatelessWidget {
|
|||
required this.onInstall,
|
||||
});
|
||||
|
||||
@override
|
||||
State<_StoreCard> createState() => _StoreCardState();
|
||||
}
|
||||
|
||||
class _StoreCardState extends State<_StoreCard> {
|
||||
bool _hovered = false;
|
||||
|
||||
StoreItem get item => widget.item;
|
||||
String get locale => widget.locale;
|
||||
String? get installedVersion => widget.installedVersion;
|
||||
VoidCallback get onTap => widget.onTap;
|
||||
VoidCallback get onInstall => widget.onInstall;
|
||||
|
||||
/// True iff the operator has this module installed AND the
|
||||
/// store-index offers a strictly-newer best_version. Uses a
|
||||
/// dotted-numeric semver-light compare so 0.10.5 > 0.9.99.
|
||||
|
|
@ -1836,146 +1849,167 @@ class _StoreCard extends StatelessWidget {
|
|||
final notInstallable = !installable && !item.installed;
|
||||
return Opacity(
|
||||
opacity: notInstallable ? 0.6 : 1.0,
|
||||
child: Material(
|
||||
color: theme.colorScheme.surfaceContainer,
|
||||
borderRadius: BorderRadius.circular(FaiRadius.md),
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(FaiRadius.md),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(FaiSpace.md),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
// Hover-lift: a 2px rise plus shadow bump signals the card
|
||||
// is clickable without a heavy hover fill. Resting state
|
||||
// carries the low elevation so cards read as physical.
|
||||
child: MouseRegion(
|
||||
onEnter: (_) => setState(() => _hovered = true),
|
||||
onExit: (_) => setState(() => _hovered = false),
|
||||
child: AnimatedContainer(
|
||||
duration: FaiMotion.base,
|
||||
curve: FaiMotion.easing,
|
||||
transform: Matrix4.translationValues(0, _hovered ? -2 : 0, 0),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(FaiRadius.md),
|
||||
boxShadow: _hovered
|
||||
? FaiElevation.medium(theme.brightness)
|
||||
: FaiElevation.low(theme.brightness),
|
||||
),
|
||||
child: Material(
|
||||
color: theme.colorScheme.surfaceContainer,
|
||||
borderRadius: BorderRadius.circular(FaiRadius.md),
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(FaiRadius.md),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(FaiSpace.md),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
CircleAvatar(
|
||||
radius: 18,
|
||||
backgroundColor: theme.colorScheme.primary.withValues(
|
||||
alpha: 0.12,
|
||||
),
|
||||
child: Icon(
|
||||
_iconForCategory(item.category),
|
||||
size: 18,
|
||||
color: theme.colorScheme.primary,
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
CircleAvatar(
|
||||
radius: 18,
|
||||
backgroundColor: theme.colorScheme.primary.withValues(
|
||||
alpha: 0.12,
|
||||
),
|
||||
child: Icon(
|
||||
_iconForCategory(item.category),
|
||||
size: 18,
|
||||
color: theme.colorScheme.primary,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: FaiSpace.sm),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
item.name,
|
||||
style: theme.textTheme.titleSmall?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
Text(
|
||||
item.category.isEmpty
|
||||
? '—'
|
||||
: _categoryDisplayName(
|
||||
context,
|
||||
item.category,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (_hasUpdate)
|
||||
Tooltip(
|
||||
message: l.storeUpdateTooltip(
|
||||
installedVersion ?? '?',
|
||||
item.bestVersion,
|
||||
),
|
||||
child: FaiPill(
|
||||
label: l.storePillUpdate,
|
||||
tone: FaiPillTone.warning,
|
||||
icon: Icons.system_update_alt,
|
||||
),
|
||||
)
|
||||
else if (item.installed)
|
||||
FaiPill(
|
||||
label: l.storePillInstalled,
|
||||
tone: FaiPillTone.success,
|
||||
icon: Icons.check,
|
||||
)
|
||||
else if (item.status.isNotEmpty)
|
||||
FaiPill(
|
||||
label: _statusDisplayName(context, item.status),
|
||||
tone: _toneForStatus(item.status),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(width: FaiSpace.sm),
|
||||
const SizedBox(height: FaiSpace.sm),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
item.name,
|
||||
style: theme.textTheme.titleSmall?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
Text(
|
||||
item.category.isEmpty
|
||||
? '—'
|
||||
: _categoryDisplayName(context, item.category),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
child: Text(
|
||||
tagline.isEmpty ? l.storeNoTagline : tagline,
|
||||
style: theme.textTheme.bodySmall,
|
||||
maxLines: 3,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
if (_hasUpdate)
|
||||
Tooltip(
|
||||
message: l.storeUpdateTooltip(
|
||||
installedVersion ?? '?',
|
||||
item.bestVersion,
|
||||
),
|
||||
child: FaiPill(
|
||||
label: l.storePillUpdate,
|
||||
tone: FaiPillTone.warning,
|
||||
icon: Icons.system_update_alt,
|
||||
),
|
||||
)
|
||||
else if (item.installed)
|
||||
FaiPill(
|
||||
label: l.storePillInstalled,
|
||||
tone: FaiPillTone.success,
|
||||
icon: Icons.check,
|
||||
)
|
||||
else if (item.status.isNotEmpty)
|
||||
FaiPill(
|
||||
label: _statusDisplayName(context, item.status),
|
||||
tone: _toneForStatus(item.status),
|
||||
),
|
||||
const SizedBox(height: FaiSpace.sm),
|
||||
Row(
|
||||
children: [
|
||||
if (item.bestVersion.isNotEmpty) ...[
|
||||
Tooltip(
|
||||
message: item.isFederated
|
||||
? l.storeAdvisoryVersionTooltip
|
||||
: '',
|
||||
child: FaiPill(
|
||||
label: item.isFederated
|
||||
? '~v${item.bestVersion}'
|
||||
: 'v${item.bestVersion}',
|
||||
tone: FaiPillTone.neutral,
|
||||
monospace: true,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: FaiSpace.xs),
|
||||
],
|
||||
_ProvenancePill(item: item),
|
||||
const Spacer(),
|
||||
if (_hasUpdate)
|
||||
FilledButton.icon(
|
||||
onPressed: onInstall,
|
||||
icon: const Icon(Icons.system_update_alt, size: 14),
|
||||
label: Text(l.storeUpdateButton(item.bestVersion)),
|
||||
style: FilledButton.styleFrom(
|
||||
visualDensity: VisualDensity.compact,
|
||||
),
|
||||
)
|
||||
else if (item.installed)
|
||||
TextButton.icon(
|
||||
onPressed: onTap,
|
||||
icon: const Icon(Icons.info_outline, size: 14),
|
||||
label: Text(l.buttonDetails),
|
||||
)
|
||||
else if (installable)
|
||||
FilledButton.icon(
|
||||
onPressed: onInstall,
|
||||
icon: const Icon(Icons.download, size: 14),
|
||||
label: Text(l.buttonInstall),
|
||||
style: FilledButton.styleFrom(
|
||||
visualDensity: VisualDensity.compact,
|
||||
),
|
||||
)
|
||||
else
|
||||
OutlinedButton.icon(
|
||||
onPressed: onTap,
|
||||
icon: const Icon(Icons.info_outline, size: 14),
|
||||
label: Text(l.buttonDetails),
|
||||
style: OutlinedButton.styleFrom(
|
||||
visualDensity: VisualDensity.compact,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: FaiSpace.sm),
|
||||
Expanded(
|
||||
child: Text(
|
||||
tagline.isEmpty ? l.storeNoTagline : tagline,
|
||||
style: theme.textTheme.bodySmall,
|
||||
maxLines: 3,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: FaiSpace.sm),
|
||||
Row(
|
||||
children: [
|
||||
if (item.bestVersion.isNotEmpty) ...[
|
||||
Tooltip(
|
||||
message: item.isFederated
|
||||
? l.storeAdvisoryVersionTooltip
|
||||
: '',
|
||||
child: FaiPill(
|
||||
label: item.isFederated
|
||||
? '~v${item.bestVersion}'
|
||||
: 'v${item.bestVersion}',
|
||||
tone: FaiPillTone.neutral,
|
||||
monospace: true,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: FaiSpace.xs),
|
||||
],
|
||||
_ProvenancePill(item: item),
|
||||
const Spacer(),
|
||||
if (_hasUpdate)
|
||||
FilledButton.icon(
|
||||
onPressed: onInstall,
|
||||
icon: const Icon(Icons.system_update_alt, size: 14),
|
||||
label: Text(l.storeUpdateButton(item.bestVersion)),
|
||||
style: FilledButton.styleFrom(
|
||||
visualDensity: VisualDensity.compact,
|
||||
),
|
||||
)
|
||||
else if (item.installed)
|
||||
TextButton.icon(
|
||||
onPressed: onTap,
|
||||
icon: const Icon(Icons.info_outline, size: 14),
|
||||
label: Text(l.buttonDetails),
|
||||
)
|
||||
else if (installable)
|
||||
FilledButton.icon(
|
||||
onPressed: onInstall,
|
||||
icon: const Icon(Icons.download, size: 14),
|
||||
label: Text(l.buttonInstall),
|
||||
style: FilledButton.styleFrom(
|
||||
visualDensity: VisualDensity.compact,
|
||||
),
|
||||
)
|
||||
else
|
||||
OutlinedButton.icon(
|
||||
onPressed: onTap,
|
||||
icon: const Icon(Icons.info_outline, size: 14),
|
||||
label: Text(l.buttonDetails),
|
||||
style: OutlinedButton.styleFrom(
|
||||
visualDensity: VisualDensity.compact,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
@ -2003,6 +2037,7 @@ class _StoreDetailSheet extends StatefulWidget {
|
|||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Theme.of(context).colorScheme.surfaceContainer,
|
||||
elevation: 8,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(FaiRadius.md)),
|
||||
),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue