feat(ui): align inventory + detail with mockups (seed icons, green theme)
Visual pass toward docs/mockups (06 inventory, 07 item): - Bundle the seedks icon font (chars a–k → seed pictograms: jar, packet, spoon…) and a SeedGlyph/QuantityKindIcon helper. - Green app bar + pale-green canvas theme (buildTaneTheme). - Inventory list: rounded-square photo thumbnail or green initial avatar, bold title, scientific-name subtitle, trailing edit pencil, styled category headers, rounded search field, seed-glyph empty state. - Quantity selectors (quick-add + add-lot) show the seed pictogram + word; detail lot lines use the kind's glyph. - Detail header restyled to mockup 07: category link + scientific name on the left, photo on the right. watchInventory now also returns the linked species' scientific name (subtitle). 37 app tests green; seedks font verified in the Linux asset bundle.
This commit is contained in:
parent
e41ec9872b
commit
48e9d15772
12 changed files with 329 additions and 49 deletions
BIN
apps/app_seeds/fonts/seedks.ttf
Normal file
BIN
apps/app_seeds/fonts/seedks.ttf
Normal file
Binary file not shown.
|
|
@ -9,6 +9,7 @@ import 'i18n/strings.g.dart';
|
|||
import 'state/inventory_cubit.dart';
|
||||
import 'state/variety_detail_cubit.dart';
|
||||
import 'ui/inventory_list_screen.dart';
|
||||
import 'ui/theme.dart';
|
||||
import 'ui/variety_detail_screen.dart';
|
||||
|
||||
/// Root widget. Provides the repository + inventory cubit to the tree and wires
|
||||
|
|
@ -54,10 +55,7 @@ class TaneApp extends StatelessWidget {
|
|||
child: MaterialApp.router(
|
||||
onGenerateTitle: (context) => context.t.app.title,
|
||||
debugShowCheckedModeBanner: false,
|
||||
theme: ThemeData(
|
||||
colorSchemeSeed: const Color(0xFF4C7A34), // seed-green
|
||||
useMaterial3: true,
|
||||
),
|
||||
theme: buildTaneTheme(),
|
||||
locale: TranslationProvider.of(context).flutterLocale,
|
||||
supportedLocales: AppLocaleUtils.supportedLocales,
|
||||
localizationsDelegates: const [
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ class VarietyListItem extends Equatable {
|
|||
required this.id,
|
||||
required this.label,
|
||||
this.category,
|
||||
this.scientificName,
|
||||
this.photo,
|
||||
});
|
||||
|
||||
|
|
@ -19,11 +20,14 @@ class VarietyListItem extends Equatable {
|
|||
final String label;
|
||||
final String? category;
|
||||
|
||||
/// Scientific name of the linked species, shown as the tile subtitle.
|
||||
final String? scientificName;
|
||||
|
||||
/// First photo (encrypted BLOB) for the avatar, or null → show an initial.
|
||||
final Uint8List? photo;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [id, label, category, photo];
|
||||
List<Object?> get props => [id, label, category, scientificName, photo];
|
||||
}
|
||||
|
||||
/// One germination test on a lot; [rate] is derived (0..1), null when it can't
|
||||
|
|
@ -154,12 +158,18 @@ class VarietyRepository {
|
|||
]);
|
||||
return query.watch().asyncMap((rows) async {
|
||||
final photos = await _firstPhotosFor(rows.map((v) => v.id).toList());
|
||||
final sciNames = await _scientificNamesFor(
|
||||
rows.map((v) => v.speciesId).whereType<String>().toSet(),
|
||||
);
|
||||
return rows
|
||||
.map(
|
||||
(v) => VarietyListItem(
|
||||
id: v.id,
|
||||
label: v.label,
|
||||
category: v.category,
|
||||
scientificName: v.speciesId == null
|
||||
? null
|
||||
: sciNames[v.speciesId],
|
||||
photo: photos[v.id],
|
||||
),
|
||||
)
|
||||
|
|
@ -189,6 +199,17 @@ class VarietyRepository {
|
|||
return byVariety;
|
||||
}
|
||||
|
||||
/// Maps each of [speciesIds] to its scientific name (one query).
|
||||
Future<Map<String, String>> _scientificNamesFor(
|
||||
Set<String> speciesIds,
|
||||
) async {
|
||||
if (speciesIds.isEmpty) return const {};
|
||||
final rows = await (_db.select(
|
||||
_db.species,
|
||||
)..where((s) => s.id.isIn(speciesIds))).get();
|
||||
return {for (final s in rows) s.id: s.scientificName};
|
||||
}
|
||||
|
||||
/// The 20-second quick-add: a [label] (required) plus an optional [category],
|
||||
/// an optional [quantity] (creates a Lot) and an optional [photoBytes]
|
||||
/// (stored as an encrypted BLOB Attachment). Returns the new Variety id.
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ import '../data/variety_repository.dart';
|
|||
import '../i18n/strings.g.dart';
|
||||
import '../state/inventory_cubit.dart';
|
||||
import 'quick_add_sheet.dart';
|
||||
import 'seed_glyph.dart';
|
||||
import 'theme.dart';
|
||||
|
||||
/// The inventory home: a searchable list of seeds grouped by category, with a
|
||||
/// quick-add FAB. Driven by [InventoryCubit] over the encrypted DB stream.
|
||||
|
|
@ -34,13 +36,19 @@ class InventoryListScreen extends StatelessWidget {
|
|||
return Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
padding: const EdgeInsets.fromLTRB(12, 12, 12, 4),
|
||||
child: TextField(
|
||||
key: const Key('inventory.search'),
|
||||
decoration: InputDecoration(
|
||||
hintText: t.inventory.searchHint,
|
||||
prefixIcon: const Icon(Icons.search),
|
||||
border: const OutlineInputBorder(),
|
||||
filled: true,
|
||||
fillColor: Colors.white,
|
||||
isDense: true,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
),
|
||||
onChanged: context.read<InventoryCubit>().search,
|
||||
),
|
||||
|
|
@ -66,10 +74,21 @@ class _InventoryBody extends StatelessWidget {
|
|||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Text(
|
||||
t.inventory.empty,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
SeedGlyph(
|
||||
SeedGlyphs.jars,
|
||||
size: 72,
|
||||
color: seedGreen.withValues(alpha: 0.5),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
t.inventory.empty,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
@ -99,11 +118,12 @@ class _CategoryHeader extends StatelessWidget {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 16, 16, 4),
|
||||
padding: const EdgeInsets.fromLTRB(16, 18, 16, 6),
|
||||
child: Text(
|
||||
title,
|
||||
style: Theme.of(context).textTheme.titleSmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: seedGreenDark,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
@ -117,16 +137,53 @@ class _VarietyTile extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
void open() => context.push('/variety/${item.id}');
|
||||
return ListTile(
|
||||
leading: _Avatar(item: item),
|
||||
title: Text(
|
||||
item.label,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
subtitle: item.scientificName == null
|
||||
? null
|
||||
: Text(
|
||||
item.scientificName!,
|
||||
style: const TextStyle(fontStyle: FontStyle.italic),
|
||||
),
|
||||
trailing: IconButton(
|
||||
icon: const Icon(Icons.edit_outlined),
|
||||
color: Colors.black38,
|
||||
tooltip: context.t.common.edit,
|
||||
onPressed: open,
|
||||
),
|
||||
onTap: open,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Rounded-square photo thumbnail, or a green initial circle when there is none.
|
||||
class _Avatar extends StatelessWidget {
|
||||
const _Avatar({required this.item});
|
||||
|
||||
final VarietyListItem item;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final photo = item.photo;
|
||||
if (photo != null) {
|
||||
return ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Image.memory(photo, width: 48, height: 48, fit: BoxFit.cover),
|
||||
);
|
||||
}
|
||||
final trimmed = item.label.trim();
|
||||
final initial = trimmed.isEmpty
|
||||
? '?'
|
||||
: trimmed.substring(0, 1).toUpperCase();
|
||||
return ListTile(
|
||||
leading: item.photo != null
|
||||
? CircleAvatar(backgroundImage: MemoryImage(item.photo!))
|
||||
: CircleAvatar(child: Text(initial)),
|
||||
title: Text(item.label),
|
||||
onTap: () => context.push('/variety/${item.id}'),
|
||||
return CircleAvatar(
|
||||
backgroundColor: seedGreen.withValues(alpha: 0.15),
|
||||
foregroundColor: seedGreenDark,
|
||||
child: Text(initial),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import '../data/variety_repository.dart';
|
|||
import '../i18n/strings.g.dart';
|
||||
import '../state/quick_add_cubit.dart';
|
||||
import 'quantity_kind_l10n.dart';
|
||||
import 'seed_glyph.dart';
|
||||
|
||||
/// Picks a photo and returns its bytes (or null if cancelled). Injected so
|
||||
/// widget tests can supply a fake instead of the camera plugin.
|
||||
|
|
@ -103,6 +104,7 @@ class QuickAddSheet extends StatelessWidget {
|
|||
children: [
|
||||
for (final kind in _quickKinds)
|
||||
ChoiceChip(
|
||||
avatar: QuantityKindIcon(kind, size: 18),
|
||||
label: Text(quantityKindLabel(t, kind)),
|
||||
selected: state.quantityKind == kind,
|
||||
onSelected: (_) => cubit.selectQuantity(kind),
|
||||
|
|
|
|||
87
apps/app_seeds/lib/ui/seed_glyph.dart
Normal file
87
apps/app_seeds/lib/ui/seed_glyph.dart
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
import 'package:commons_core/commons_core.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Renders a glyph from the bundled `seedks` icon font, whose characters `a`–`k`
|
||||
/// map to seed-themed pictograms (jar, packet, spoon…). See
|
||||
/// docs/mockups/seedks-glyphs.png.
|
||||
class SeedGlyph extends StatelessWidget {
|
||||
const SeedGlyph(this.char, {this.size = 24, this.color, super.key});
|
||||
|
||||
final String char;
|
||||
final double size;
|
||||
final Color? color;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Text(
|
||||
char,
|
||||
style: TextStyle(
|
||||
fontFamily: 'seedks',
|
||||
fontSize: size,
|
||||
height: 1,
|
||||
color: color ?? Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Named seedks glyphs (the character each pictogram is mapped to).
|
||||
abstract final class SeedGlyphs {
|
||||
static const jar = 'a';
|
||||
static const jars = 'b';
|
||||
static const search = 'c';
|
||||
static const share = 'd';
|
||||
static const sack = 'e';
|
||||
static const smallSpoon = 'f';
|
||||
static const bigSpoon = 'g';
|
||||
static const mug = 'h';
|
||||
static const envelope = 'i';
|
||||
static const pouring = 'j';
|
||||
static const scattered = 'k';
|
||||
}
|
||||
|
||||
/// The seedks character for a quantity [kind], or null for plant-form/precise
|
||||
/// kinds that have no seed pictogram (they fall back to a Material icon).
|
||||
String? seedGlyphForKind(QuantityKind kind) => switch (kind) {
|
||||
QuantityKind.aFew => SeedGlyphs.scattered,
|
||||
QuantityKind.some => SeedGlyphs.smallSpoon,
|
||||
QuantityKind.plenty => SeedGlyphs.jars,
|
||||
QuantityKind.handful => SeedGlyphs.bigSpoon,
|
||||
QuantityKind.pinch => SeedGlyphs.scattered,
|
||||
QuantityKind.jar => SeedGlyphs.jar,
|
||||
QuantityKind.packet => SeedGlyphs.envelope,
|
||||
_ => null,
|
||||
};
|
||||
|
||||
IconData _materialIconForKind(QuantityKind kind) => switch (kind) {
|
||||
QuantityKind.grams => Icons.scale_outlined,
|
||||
QuantityKind.count => Icons.tag,
|
||||
QuantityKind.cob || QuantityKind.ear => Icons.grass,
|
||||
QuantityKind.head || QuantityKind.seedHead => Icons.filter_vintage_outlined,
|
||||
QuantityKind.fruit => Icons.local_florist_outlined,
|
||||
QuantityKind.bulb ||
|
||||
QuantityKind.tuber ||
|
||||
QuantityKind.bunch => Icons.spa_outlined,
|
||||
_ => Icons.eco_outlined,
|
||||
};
|
||||
|
||||
/// A consistent icon for a [QuantityKind]: the seed glyph where one exists,
|
||||
/// otherwise a Material fallback.
|
||||
class QuantityKindIcon extends StatelessWidget {
|
||||
const QuantityKindIcon(this.kind, {this.size = 24, this.color, super.key});
|
||||
|
||||
final QuantityKind kind;
|
||||
final double size;
|
||||
final Color? color;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final glyph = seedGlyphForKind(kind);
|
||||
if (glyph != null) return SeedGlyph(glyph, size: size, color: color);
|
||||
return Icon(
|
||||
_materialIconForKind(kind),
|
||||
size: size,
|
||||
color: color ?? Theme.of(context).colorScheme.primary,
|
||||
);
|
||||
}
|
||||
}
|
||||
35
apps/app_seeds/lib/ui/theme.dart
Normal file
35
apps/app_seeds/lib/ui/theme.dart
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Seed-green palette drawn from the mockups (docs/mockups): a green app bar and
|
||||
/// a pale-green canvas.
|
||||
const seedGreen = Color(0xFF3C8B2E);
|
||||
const seedGreenDark = Color(0xFF1B5E20);
|
||||
const seedCanvas = Color(0xFFEAF4E2);
|
||||
const seedLink = Color(0xFF1E88C0);
|
||||
|
||||
ThemeData buildTaneTheme() {
|
||||
final scheme = ColorScheme.fromSeed(
|
||||
seedColor: seedGreen,
|
||||
brightness: Brightness.light,
|
||||
);
|
||||
return ThemeData(
|
||||
useMaterial3: true,
|
||||
colorScheme: scheme,
|
||||
scaffoldBackgroundColor: seedCanvas,
|
||||
appBarTheme: const AppBarTheme(
|
||||
backgroundColor: seedGreen,
|
||||
foregroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
centerTitle: false,
|
||||
),
|
||||
floatingActionButtonTheme: const FloatingActionButtonThemeData(
|
||||
backgroundColor: seedGreen,
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
bottomSheetTheme: const BottomSheetThemeData(
|
||||
backgroundColor: seedCanvas,
|
||||
surfaceTintColor: Colors.transparent,
|
||||
),
|
||||
dividerTheme: const DividerThemeData(space: 1, thickness: 1),
|
||||
);
|
||||
}
|
||||
|
|
@ -6,6 +6,8 @@ import '../data/species_repository.dart';
|
|||
import '../data/variety_repository.dart';
|
||||
import '../i18n/strings.g.dart';
|
||||
import '../state/variety_detail_cubit.dart';
|
||||
import 'seed_glyph.dart';
|
||||
import 'theme.dart';
|
||||
import 'quantity_kind_l10n.dart';
|
||||
|
||||
/// Read + edit view of a single variety: its photo, category, notes, other
|
||||
|
|
@ -72,33 +74,7 @@ class _DetailView extends StatelessWidget {
|
|||
body: ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
children: [
|
||||
if (detail.scientificName != null) ...[
|
||||
Text(
|
||||
detail.scientificName!,
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
fontStyle: FontStyle.italic,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
],
|
||||
if (detail.photo != null)
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Image.memory(
|
||||
detail.photo!,
|
||||
height: 200,
|
||||
width: double.infinity,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
if (detail.category != null) ...[
|
||||
const SizedBox(height: 12),
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Chip(label: Text(detail.category!)),
|
||||
),
|
||||
],
|
||||
_DetailHeader(detail: detail),
|
||||
if (detail.vernacularNames.isNotEmpty) ...[
|
||||
const SizedBox(height: 16),
|
||||
_SectionTitle(t.detail.names),
|
||||
|
|
@ -134,7 +110,9 @@ class _DetailView extends StatelessWidget {
|
|||
for (final lot in detail.lots)
|
||||
ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
leading: const Icon(Icons.inventory_2_outlined),
|
||||
leading: lot.quantity == null
|
||||
? const Icon(Icons.inventory_2_outlined)
|
||||
: QuantityKindIcon(lot.quantity!.kind, size: 28),
|
||||
title: Text(_lotSubtitle(t, lot)),
|
||||
subtitle: lot.storageLocation == null
|
||||
? null
|
||||
|
|
@ -548,6 +526,7 @@ Future<void> _showAddLotSheet(BuildContext context, VarietyDetailCubit cubit) {
|
|||
children: [
|
||||
for (final kind in _addLotKinds)
|
||||
ChoiceChip(
|
||||
avatar: QuantityKindIcon(kind, size: 18),
|
||||
label: Text(quantityKindLabel(t, kind)),
|
||||
selected: selectedKind == kind,
|
||||
onSelected: (_) => setState(() => selectedKind = kind),
|
||||
|
|
@ -598,6 +577,61 @@ String? _nullIfBlank(String value) {
|
|||
return trimmed.isEmpty ? null : trimmed;
|
||||
}
|
||||
|
||||
/// Mockup-07 header: category link + scientific name on the left, the photo on
|
||||
/// the right.
|
||||
class _DetailHeader extends StatelessWidget {
|
||||
const _DetailHeader({required this.detail});
|
||||
|
||||
final VarietyDetail detail;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final hasText = detail.category != null || detail.scientificName != null;
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (detail.category != null)
|
||||
Text(
|
||||
detail.category!,
|
||||
style: const TextStyle(
|
||||
color: seedLink,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
if (detail.scientificName != null) ...[
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
detail.scientificName!,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
fontStyle: FontStyle.italic,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
if (detail.photo != null) ...[
|
||||
if (hasText) const SizedBox(width: 12),
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Image.memory(
|
||||
detail.photo!,
|
||||
width: 140,
|
||||
height: 140,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SectionTitle extends StatelessWidget {
|
||||
const _SectionTitle(this.text);
|
||||
|
||||
|
|
|
|||
|
|
@ -66,3 +66,8 @@ flutter:
|
|||
# Seed strings for slang live under lib/i18n (not a Flutter asset; compiled).
|
||||
assets:
|
||||
- assets/catalog/species.json
|
||||
fonts:
|
||||
# Custom seed-themed icon glyphs (chars a–k). See docs/mockups/seedks-glyphs.png.
|
||||
- family: seedks
|
||||
fonts:
|
||||
- asset: fonts/seedks.ttf
|
||||
|
|
|
|||
|
|
@ -83,6 +83,10 @@ void main() {
|
|||
final detail = await repo.watchVariety(id).first;
|
||||
expect(detail!.scientificName, 'Solanum lycopersicum');
|
||||
expect(detail.category, 'Solanaceae'); // prefilled from family
|
||||
|
||||
// The inventory list row also surfaces the scientific name (subtitle).
|
||||
final row = (await repo.watchInventory().first).single;
|
||||
expect(row.scientificName, 'Solanum lycopersicum');
|
||||
},
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -37,11 +37,48 @@ JSON por locale, compatible Weblate). Ver [`../../apps/app_seeds`](../../apps/ap
|
|||
|
||||
- **Editar nombres vernáculos** ("también conocida como": hoy solo lectura;
|
||||
falta añadir/quitar — colección OR-Set).
|
||||
- **Editar/borrar lotes** (se pueden añadir pero no modificar/eliminar).
|
||||
- **Editar/borrar lotes** (se pueden añadir pero no modificar/eliminar). Falta
|
||||
`updateLot`/`softDeleteLot` en el repo (hoy solo `addLot` en
|
||||
[`variety_repository.dart`](../../apps/app_seeds/lib/data/variety_repository.dart))
|
||||
y la afordancia en las tiles de lote de
|
||||
[`variety_detail_screen.dart`](../../apps/app_seeds/lib/ui/variety_detail_screen.dart).
|
||||
El soft-delete ya tiene infraestructura (`isDeleted`/tombstone vía `SyncColumns`).
|
||||
- **Editar cantidad del lote**: la UI de alta solo ofrece 6 `QuantityKind` como
|
||||
chips; falta campo para cantidad numérica (`precise`) y etiqueta libre (`label`),
|
||||
que el modelo (`Quantity`) ya soporta.
|
||||
- (Opcional) QR de recuperación de la semilla raíz; almacenamiento cifrado de
|
||||
fotos fuera de la BD (hoy van como BLOB cifrado dentro de la BD, que cumple la
|
||||
regla).
|
||||
|
||||
### Feedback de prueba (vjrj, 2026-07-07)
|
||||
|
||||
Tras probar la app: no se pueden editar unidades/cantidad ni editar/borrar lotes
|
||||
(solo añadir) — **esperado**, cubierto por los pendientes de arriba. Echa en falta
|
||||
la iconografía de los mockups (ver nota siguiente).
|
||||
|
||||
## Iconografía de los mockups (diseño futuro) 🎨
|
||||
|
||||
La app usa hoy **Material Icons genéricos**. Los iconos de los mockups **existen** ya
|
||||
como font custom: **[`seedks.ttf`](../../seedks.ttf)** (raíz del repo, trackeado en
|
||||
git), familia interna **"Seedks"**, **11 glifos** en `a`–`k` (U+0061–U+006B), de la
|
||||
misma época que [`../icons.svg`](../icons.svg) (2018). Preview renderizado:
|
||||
[`../mockups/seedks-glyphs.png`](../mockups/seedks-glyphs.png).
|
||||
|
||||
Mapeo de glifos:
|
||||
|
||||
| Glifo | Icono | Glifo | Icono | Glifo | Icono |
|
||||
|---|---|---|---|---|---|
|
||||
| `a` | tarro | `e` | saco/bolsa | `i` | sobre |
|
||||
| `b` | tarros (varios) | `f` | cucharilla | `j` | sobre vertiendo |
|
||||
| `c` | búsqueda (lupa) | `g` | cuchara | `k` | semillas sueltas |
|
||||
| `d` | mano compartiendo | `h` | taza | | |
|
||||
|
||||
**No** está cableado: falta declarar `fonts:` en
|
||||
[`../../apps/app_seeds/pubspec.yaml`](../../apps/app_seeds/pubspec.yaml) y crear un
|
||||
mapa codepoint→`IconData` (`fontFamily: 'Seedks'`). Recuperar la iconografía **no**
|
||||
requiere SVGs nuevos. Iconos por familia/categoría y por unidad de cantidad =
|
||||
trabajo de diseño para después del pulido del Bloque 1.
|
||||
|
||||
## Fuera de alcance (Bloque 2, no empezar) 🚫
|
||||
|
||||
Ofertas, mensajería, relays, Nostr, red de confianza, Ğ1, sincronización entre
|
||||
|
|
|
|||
BIN
docs/mockups/seedks-glyphs.png
Normal file
BIN
docs/mockups/seedks-glyphs.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 29 KiB |
Loading…
Add table
Add a link
Reference in a new issue