kasy-cli 1.14.0 → 1.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/bin/kasy.js +18 -5
  2. package/lib/commands/ios.js +8 -2
  3. package/lib/commands/reset.js +100 -2
  4. package/lib/commands/splash.js +11 -0
  5. package/lib/scaffold/backends/api/pubspec.yaml.tpl +2 -2
  6. package/lib/scaffold/backends/supabase/pubspec.yaml.tpl +2 -2
  7. package/lib/utils/apple-release.js +30 -0
  8. package/lib/utils/checks.js +41 -2
  9. package/lib/utils/debug.js +75 -0
  10. package/lib/utils/friendly-error.js +91 -0
  11. package/lib/utils/i18n/messages-en.js +970 -0
  12. package/lib/utils/i18n/messages-es.js +968 -0
  13. package/lib/utils/i18n/messages-pt.js +968 -0
  14. package/lib/utils/i18n.js +21 -2818
  15. package/lib/utils/png-padding.js +120 -0
  16. package/package.json +8 -3
  17. package/templates/firebase/android/app/src/main/kotlin/com/aicrus/firebase/kit/MyWidget.kt +12 -11
  18. package/templates/firebase/android/app/src/main/res/drawable-hdpi/android12splash.png +0 -0
  19. package/templates/firebase/android/app/src/main/res/drawable-hdpi/ic_launcher_background.png +0 -0
  20. package/templates/firebase/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png +0 -0
  21. package/templates/firebase/android/app/src/main/res/drawable-mdpi/android12splash.png +0 -0
  22. package/templates/firebase/android/app/src/main/res/drawable-mdpi/ic_launcher_background.png +0 -0
  23. package/templates/firebase/android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png +0 -0
  24. package/templates/firebase/android/app/src/main/res/drawable-night-hdpi/android12splash.png +0 -0
  25. package/templates/firebase/android/app/src/main/res/drawable-night-mdpi/android12splash.png +0 -0
  26. package/templates/firebase/android/app/src/main/res/drawable-night-xhdpi/android12splash.png +0 -0
  27. package/templates/firebase/android/app/src/main/res/drawable-night-xxhdpi/android12splash.png +0 -0
  28. package/templates/firebase/android/app/src/main/res/drawable-night-xxxhdpi/android12splash.png +0 -0
  29. package/templates/firebase/android/app/src/main/res/drawable-xhdpi/android12splash.png +0 -0
  30. package/templates/firebase/android/app/src/main/res/drawable-xhdpi/ic_launcher_background.png +0 -0
  31. package/templates/firebase/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png +0 -0
  32. package/templates/firebase/android/app/src/main/res/drawable-xxhdpi/android12splash.png +0 -0
  33. package/templates/firebase/android/app/src/main/res/drawable-xxhdpi/ic_launcher_background.png +0 -0
  34. package/templates/firebase/android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png +0 -0
  35. package/templates/firebase/android/app/src/main/res/drawable-xxxhdpi/android12splash.png +0 -0
  36. package/templates/firebase/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_background.png +0 -0
  37. package/templates/firebase/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png +0 -0
  38. package/templates/firebase/android/app/src/main/res/layout/widget_preview.xml +18 -11
  39. package/templates/firebase/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +9 -0
  40. package/templates/firebase/assets/images/icon_android.png +0 -0
  41. package/templates/firebase/assets/images/icon_foreground_empty.png +0 -0
  42. package/templates/firebase/assets/images/splash_logo_dark_android12.png +0 -0
  43. package/templates/firebase/assets/images/splash_logo_light_android12.png +0 -0
  44. package/templates/firebase/lib/components/components.dart +1 -0
  45. package/templates/firebase/lib/components/kasy_avatar.dart +88 -57
  46. package/templates/firebase/lib/components/kasy_avatar_presets.dart +116 -74
  47. package/templates/firebase/lib/components/kasy_tabs.dart +431 -0
  48. package/templates/firebase/lib/core/home_widgets/home_widget_mywidget_service.dart +12 -6
  49. package/templates/firebase/lib/features/home/home_components_page.dart +1 -1
  50. package/templates/firebase/lib/features/home/home_components_preview_registry.dart +316 -93
  51. package/templates/firebase/pubspec.yaml +4 -2
  52. package/templates/firebase/web/index.html +6 -0
@@ -0,0 +1,120 @@
1
+ const fsp = require('node:fs/promises');
2
+ const { PNG } = require('pngjs');
3
+
4
+ const ANDROID12_SAFE_RATIO = 0.5;
5
+
6
+ async function readPng(filePath) {
7
+ const buffer = await fsp.readFile(filePath);
8
+ return new Promise((resolve, reject) => {
9
+ new PNG().parse(buffer, (err, data) => {
10
+ if (err) reject(err);
11
+ else resolve(data);
12
+ });
13
+ });
14
+ }
15
+
16
+ function resizeBilinear(src, dstW, dstH) {
17
+ const dst = new PNG({ width: dstW, height: dstH });
18
+ const srcW = src.width;
19
+ const srcH = src.height;
20
+ const xRatio = srcW > 1 ? (srcW - 1) / dstW : 0;
21
+ const yRatio = srcH > 1 ? (srcH - 1) / dstH : 0;
22
+
23
+ for (let y = 0; y < dstH; y++) {
24
+ const sy = (y + 0.5) * yRatio;
25
+ const y0 = Math.floor(sy);
26
+ const y1 = Math.min(y0 + 1, srcH - 1);
27
+ const wy = sy - y0;
28
+
29
+ for (let x = 0; x < dstW; x++) {
30
+ const sx = (x + 0.5) * xRatio;
31
+ const x0 = Math.floor(sx);
32
+ const x1 = Math.min(x0 + 1, srcW - 1);
33
+ const wx = sx - x0;
34
+
35
+ const i00 = (y0 * srcW + x0) * 4;
36
+ const i10 = (y0 * srcW + x1) * 4;
37
+ const i01 = (y1 * srcW + x0) * 4;
38
+ const i11 = (y1 * srcW + x1) * 4;
39
+
40
+ const dstIdx = (y * dstW + x) * 4;
41
+ for (let c = 0; c < 4; c++) {
42
+ const top = src.data[i00 + c] * (1 - wx) + src.data[i10 + c] * wx;
43
+ const bot = src.data[i01 + c] * (1 - wx) + src.data[i11 + c] * wx;
44
+ dst.data[dstIdx + c] = Math.round(top * (1 - wy) + bot * wy);
45
+ }
46
+ }
47
+ }
48
+ return dst;
49
+ }
50
+
51
+ function compositeOnTransparentSquare(logo, canvasSize) {
52
+ const canvas = new PNG({ width: canvasSize, height: canvasSize });
53
+ canvas.data.fill(0);
54
+
55
+ const offsetX = Math.floor((canvasSize - logo.width) / 2);
56
+ const offsetY = Math.floor((canvasSize - logo.height) / 2);
57
+
58
+ for (let y = 0; y < logo.height; y++) {
59
+ for (let x = 0; x < logo.width; x++) {
60
+ const srcIdx = (y * logo.width + x) * 4;
61
+ const dstIdx = ((y + offsetY) * canvasSize + (x + offsetX)) * 4;
62
+ canvas.data[dstIdx] = logo.data[srcIdx];
63
+ canvas.data[dstIdx + 1] = logo.data[srcIdx + 1];
64
+ canvas.data[dstIdx + 2] = logo.data[srcIdx + 2];
65
+ canvas.data[dstIdx + 3] = logo.data[srcIdx + 3];
66
+ }
67
+ }
68
+ return canvas;
69
+ }
70
+
71
+ async function writePng(png, filePath) {
72
+ return new Promise((resolve, reject) => {
73
+ const chunks = [];
74
+ png.pack()
75
+ .on('data', (chunk) => chunks.push(chunk))
76
+ .on('end', async () => {
77
+ try {
78
+ await fsp.writeFile(filePath, Buffer.concat(chunks));
79
+ resolve();
80
+ } catch (e) {
81
+ reject(e);
82
+ }
83
+ })
84
+ .on('error', reject);
85
+ });
86
+ }
87
+
88
+ /**
89
+ * Read a PNG and write a new one of the same dimensions, with the source
90
+ * logo scaled down to fit inside the Android 12+ splash safe area (centered,
91
+ * transparent padding around it). This is what `windowSplashScreenAnimatedIcon`
92
+ * needs so the OS-applied circular mask doesn't clip the logo edges.
93
+ *
94
+ * @param {string} srcPath
95
+ * @param {string} dstPath
96
+ * @param {number} safeRatio fraction of the canvas the logo should occupy (default 0.6)
97
+ */
98
+ async function writeAndroid12Variant(srcPath, dstPath, safeRatio = ANDROID12_SAFE_RATIO) {
99
+ const src = await readPng(srcPath);
100
+ const canvasSize = Math.max(src.width, src.height);
101
+ const safeSide = Math.round(canvasSize * safeRatio);
102
+
103
+ const aspect = src.width / src.height;
104
+ let logoW;
105
+ let logoH;
106
+ if (aspect >= 1) {
107
+ logoW = safeSide;
108
+ logoH = Math.round(safeSide / aspect);
109
+ } else {
110
+ logoH = safeSide;
111
+ logoW = Math.round(safeSide * aspect);
112
+ }
113
+
114
+ const resized = resizeBilinear(src, logoW, logoH);
115
+ const composited = compositeOnTransparentSquare(resized, canvasSize);
116
+ await writePng(composited, dstPath);
117
+ return { canvasSize, logoW, logoH };
118
+ }
119
+
120
+ module.exports = { writeAndroid12Variant, ANDROID12_SAFE_RATIO };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kasy-cli",
3
- "version": "1.14.0",
3
+ "version": "1.15.0",
4
4
  "description": "CLI for scaffolding production-ready Flutter SaaS apps with Firebase, Supabase, or API REST backends.",
5
5
  "bin": {
6
6
  "kasy": "./bin/kasy.js"
@@ -39,10 +39,12 @@
39
39
  "validate": "node ./bin/kasy.js validate --analyze-only",
40
40
  "extract:patch": "node ./scripts/extract_patch.js",
41
41
  "check:firebase": "node ./scripts/check-firebase-template.js",
42
+ "test": "for f in test/*.test.js; do node \"$f\" || exit 1; done",
42
43
  "test:google-ios": "node ./test/google-ios-url-scheme.test.js",
43
44
  "test:apple-release": "node ./test/apple-release.test.js",
44
45
  "test:localize-docs": "node ./test/localize-release-docs.test.js",
45
- "test:i18n-accents": "node ./test/i18n-accents.test.js"
46
+ "test:i18n-accents": "node ./test/i18n-accents.test.js",
47
+ "lint": "eslint bin lib scripts"
46
48
  },
47
49
  "dependencies": {
48
50
  "@clack/prompts": "^1.4.0",
@@ -51,7 +53,10 @@
51
53
  "fs-extra": "^11.2.0",
52
54
  "gradient-string": "^1.2.0",
53
55
  "kleur": "^4.1.5",
54
- "prompts": "^2.4.2",
56
+ "pngjs": "^7.0.0",
55
57
  "yaml": "^2.4.2"
58
+ },
59
+ "devDependencies": {
60
+ "eslint": "^9.39.4"
56
61
  }
57
62
  }
@@ -220,17 +220,18 @@ class MyWidgetWidget : GlanceAppWidget() {
220
220
  return DefaultStrings(greeting, hello)
221
221
  }
222
222
 
223
- /// Builds the same Intent the launcher would fire when the user taps the
224
- /// app icon: ACTION_MAIN + CATEGORY_LAUNCHER. Without these flags, the
225
- /// activity receives an empty Intent and go_router lands on the
226
- /// errorBuilder (the "404 - Page not found" screen).
223
+ /// Builds the exact Intent the system launcher fires when the user taps
224
+ /// the app icon. We must NOT add extra flags here — getLaunchIntentForPackage
225
+ /// already returns `FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_RESET_IF_NEEDED`,
226
+ /// which is the same combo the launcher uses. Adding `CLEAR_TOP` destroys
227
+ /// go_router's navigation stack on warm starts and lands the user on the
228
+ /// errorBuilder ("404 - Page not found").
227
229
  private fun launchAppIntent(context: Context): Intent {
228
- return Intent(context, MainActivity::class.java).apply {
229
- action = Intent.ACTION_MAIN
230
- addCategory(Intent.CATEGORY_LAUNCHER)
231
- flags = Intent.FLAG_ACTIVITY_NEW_TASK or
232
- Intent.FLAG_ACTIVITY_CLEAR_TOP or
233
- Intent.FLAG_ACTIVITY_SINGLE_TOP
234
- }
230
+ return context.packageManager.getLaunchIntentForPackage(context.packageName)
231
+ ?: Intent(context, MainActivity::class.java).apply {
232
+ action = Intent.ACTION_MAIN
233
+ addCategory(Intent.CATEGORY_LAUNCHER)
234
+ flags = Intent.FLAG_ACTIVITY_NEW_TASK
235
+ }
235
236
  }
236
237
  }
@@ -1,19 +1,21 @@
1
1
  <?xml version="1.0" encoding="utf-8"?>
2
- <!-- Static preview shown in the Android widget gallery (Android 12+).
3
- Mirrors the real widget layout so the user can see what they're
4
- adding before they drop it on the home screen. The Glance widget
5
- replaces this once placed. -->
2
+ <!-- Static preview shown in the widget gallery (Android 12+).
3
+ Only uses Views allowed by RemoteViews no <Space>, no
4
+ paddingHorizontal/Vertical, no fontFamily. Anything outside
5
+ the allowlist makes the launcher silently fall back to a gray box. -->
6
6
  <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
7
7
  android:layout_width="match_parent"
8
8
  android:layout_height="match_parent"
9
9
  android:background="@drawable/widget_gradient_bg"
10
10
  android:orientation="vertical"
11
- android:padding="16dp">
11
+ android:paddingLeft="16dp"
12
+ android:paddingRight="16dp"
13
+ android:paddingTop="16dp"
14
+ android:paddingBottom="16dp">
12
15
 
13
16
  <TextView
14
17
  android:layout_width="wrap_content"
15
18
  android:layout_height="wrap_content"
16
- android:fontFamily="sans-serif-medium"
17
19
  android:text="Boa noite"
18
20
  android:textColor="#8CFFFFFF"
19
21
  android:textSize="11sp" />
@@ -24,20 +26,25 @@
24
26
  android:layout_marginTop="4dp"
25
27
  android:text="Olá!"
26
28
  android:textColor="#FFFFFFFF"
27
- android:textSize="24sp"
29
+ android:textSize="22sp"
28
30
  android:textStyle="bold" />
29
31
 
30
- <Space
32
+ <!-- Filler row uses TextView with empty text + weight to push the pill
33
+ to the bottom (Space isn't on the RemoteViews allowlist). -->
34
+ <TextView
31
35
  android:layout_width="match_parent"
32
36
  android:layout_height="0dp"
33
- android:layout_weight="1" />
37
+ android:layout_weight="1"
38
+ android:text="" />
34
39
 
35
40
  <TextView
36
41
  android:layout_width="wrap_content"
37
42
  android:layout_height="wrap_content"
38
43
  android:background="@drawable/widget_pro_pill_bg"
39
- android:paddingHorizontal="10dp"
40
- android:paddingVertical="5dp"
44
+ android:paddingLeft="10dp"
45
+ android:paddingRight="10dp"
46
+ android:paddingTop="5dp"
47
+ android:paddingBottom="5dp"
41
48
  android:text="⭐ PRO"
42
49
  android:textColor="#FFFFD700"
43
50
  android:textSize="11sp"
@@ -0,0 +1,9 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
3
+ <background android:drawable="@drawable/ic_launcher_background"/>
4
+ <foreground>
5
+ <inset
6
+ android:drawable="@drawable/ic_launcher_foreground"
7
+ android:inset="16%" />
8
+ </foreground>
9
+ </adaptive-icon>
@@ -22,6 +22,7 @@ export 'kasy_dialog.dart';
22
22
  export 'kasy_otp_verification_bottom_sheet.dart';
23
23
  export 'kasy_skeleton.dart';
24
24
  export 'kasy_swipe_action.dart';
25
+ export 'kasy_tabs.dart';
25
26
  export 'kasy_text_area.dart';
26
27
  export 'kasy_text_field.dart';
27
28
  export 'kasy_text_field_otp.dart';
@@ -28,7 +28,7 @@ enum KasyAvatarStatus { online }
28
28
 
29
29
  /// Design-system avatar: image, initials, icon, gradient, or custom child.
30
30
  ///
31
- /// Decorative radial fills: optional [KasyAvatarOrbGradients] (barrel presets).
31
+ /// Decorative linear fills: optional [KasyAvatarGradients] (barrel presets).
32
32
  class KasyAvatar extends StatelessWidget {
33
33
  final KasyAvatarSize size;
34
34
 
@@ -60,7 +60,7 @@ class KasyAvatar extends StatelessWidget {
60
60
  this.tone = KasyAvatarTone.blue,
61
61
  this.fallbackSurface = KasyAvatarFallbackSurface.default_,
62
62
  this.shape = KasyAvatarShape.circle,
63
- this.showShadow = true,
63
+ this.showShadow = false,
64
64
  this.showStoryRing = false,
65
65
  this.storyRingGradient,
66
66
  this.status,
@@ -244,7 +244,7 @@ class KasyAvatar extends StatelessWidget {
244
244
  return ColoredBox(
245
245
  color: bg,
246
246
  child: Center(
247
- child: Icon(glyph, size: _d * 0.48, color: fg),
247
+ child: Icon(glyph, size: _d * 0.444, color: fg),
248
248
  ),
249
249
  );
250
250
  }
@@ -253,11 +253,12 @@ class KasyAvatar extends StatelessWidget {
253
253
  child: Center(
254
254
  child: Text(
255
255
  label ?? '',
256
- style: context.textTheme.titleSmall?.copyWith(
256
+ style: TextStyle(
257
257
  color: fg,
258
- fontWeight: FontWeight.w600,
259
- fontSize: _d * 0.32,
258
+ fontWeight: FontWeight.w500,
259
+ fontSize: _d * 0.333,
260
260
  height: 1,
261
+ leadingDistribution: TextLeadingDistribution.even,
261
262
  ),
262
263
  ),
263
264
  ),
@@ -320,7 +321,10 @@ class KasyAvatar extends StatelessWidget {
320
321
  }
321
322
  }
322
323
 
323
- /// Horizontal stacked avatars with [KasyColors.surface] separation ring and optional +N.
324
+ /// Horizontal stacked avatars with [KasyColors.surface] separation ring,
325
+ /// optional +N counter, and optional add-action button.
326
+ ///
327
+ /// Set [onAdd] to show the "+" button at the end (12 px gap, per Figma spec).
324
328
  class KasyAvatarGroup extends StatelessWidget {
325
329
  final List<Widget> avatars;
326
330
  final int maxVisible;
@@ -329,14 +333,18 @@ class KasyAvatarGroup extends StatelessWidget {
329
333
  final bool showShadow;
330
334
  final double itemDiameter;
331
335
 
336
+ /// When non-null a circular "+" button is shown after the group.
337
+ final VoidCallback? onAdd;
338
+
332
339
  const KasyAvatarGroup({
333
340
  super.key,
334
341
  required this.avatars,
335
342
  this.maxVisible = 4,
336
343
  this.extraCount = 0,
337
- this.overlapFactor = 0.34,
338
- this.showShadow = true,
344
+ this.overlapFactor = 0.28,
345
+ this.showShadow = false,
339
346
  this.itemDiameter = 48,
347
+ this.onAdd,
340
348
  });
341
349
 
342
350
  @override
@@ -346,9 +354,10 @@ class KasyAvatarGroup extends StatelessWidget {
346
354
  final double step = d * (1 - overlapFactor);
347
355
  final int showCounter = extraCount > 0 ? 1 : 0;
348
356
  final int totalSlots = n + showCounter;
349
- final double w = totalSlots <= 0 ? 0 : step * (totalSlots - 1) + d + 4;
350
- return SizedBox(
351
- width: w,
357
+ final double stackW = totalSlots <= 0 ? 0 : step * (totalSlots - 1) + d + 4;
358
+
359
+ final Widget stack = SizedBox(
360
+ width: stackW,
352
361
  height: d + 4,
353
362
  child: Stack(
354
363
  clipBehavior: Clip.none,
@@ -371,9 +380,14 @@ class KasyAvatarGroup extends StatelessWidget {
371
380
  child: Center(
372
381
  child: Text(
373
382
  '+$extraCount',
374
- style: context.textTheme.labelLarge?.copyWith(
375
- color: context.colors.primary,
376
- fontWeight: FontWeight.w600,
383
+ style: TextStyle(
384
+ color: context.isDark
385
+ ? const Color(0xFFFCFCFC)
386
+ : const Color(0xFF18181B),
387
+ fontWeight: FontWeight.w500,
388
+ fontSize: 12,
389
+ height: 16 / 12,
390
+ leadingDistribution: TextLeadingDistribution.even,
377
391
  ),
378
392
  ),
379
393
  ),
@@ -384,22 +398,50 @@ class KasyAvatarGroup extends StatelessWidget {
384
398
  ],
385
399
  ),
386
400
  );
401
+
402
+ if (onAdd == null) return stack;
403
+
404
+ // Add-action button: circle with "+" icon, 12 px gap (Figma layout_EJ08D4).
405
+ final Widget addBtn = _KasyAvatarPressable(
406
+ onPressed: onAdd!,
407
+ semanticLabel: 'Add member',
408
+ minSide: d < 44 ? 44.0 : d,
409
+ child: Container(
410
+ width: d,
411
+ height: d,
412
+ decoration: BoxDecoration(
413
+ color: context.colors.avatarFallbackFill,
414
+ shape: BoxShape.circle,
415
+ ),
416
+ child: Center(
417
+ child: Icon(
418
+ KasyIcons.add,
419
+ size: d * 0.444,
420
+ color: context.colors.primary,
421
+ ),
422
+ ),
423
+ ),
424
+ );
425
+
426
+ return Row(
427
+ mainAxisSize: MainAxisSize.min,
428
+ children: [stack, const SizedBox(width: 12), addBtn],
429
+ );
387
430
  }
388
431
 
389
432
  Widget _ringWrap(BuildContext context, double d, Widget child, bool shadow) {
390
- final Widget bordered = Container(
433
+ return Container(
391
434
  width: d,
392
435
  height: d,
393
436
  decoration: BoxDecoration(
394
437
  shape: BoxShape.circle,
395
- border: Border.all(color: context.colors.surface, width: 2.5),
438
+ border: Border.all(color: context.colors.surface, width: 2.0),
396
439
  boxShadow: shadow ? [KasyShadows.component(context)] : null,
397
440
  ),
398
441
  child: ClipOval(
399
442
  child: SizedBox(width: d, height: d, child: child),
400
443
  ),
401
444
  );
402
- return bordered;
403
445
  }
404
446
  }
405
447
 
@@ -418,52 +460,41 @@ _KasyAvatarColors _colorsForTone(
418
460
  final KasyColors k = context.colors;
419
461
  final bool dark = context.isDark;
420
462
 
421
- Color accent() {
422
- return switch (tone) {
423
- KasyAvatarTone.blue => k.primary,
424
- KasyAvatarTone.neutral => k.grey3,
425
- KasyAvatarTone.green => k.success,
426
- KasyAvatarTone.orange => k.warning,
427
- KasyAvatarTone.red => k.error,
428
- };
429
- }
463
+ // Accent color per tone (Figma: accent=#0485F7, success=#17C964,
464
+ // warning=#F5A524, danger=#FF383C, neutral uses neutral fg).
465
+ Color accentColor() => switch (tone) {
466
+ KasyAvatarTone.blue => k.primary,
467
+ KasyAvatarTone.neutral => k.grey3,
468
+ KasyAvatarTone.green => k.success,
469
+ KasyAvatarTone.orange => k.warning,
470
+ KasyAvatarTone.red => k.error,
471
+ };
430
472
 
473
+ // Solid surface: all types share the same #EBEBEC bg (Figma fill_J18KAR).
474
+ // Foreground: neutral → onSurface (#18181B light), others → accent color.
431
475
  if (fallbackSurface == KasyAvatarFallbackSurface.default_) {
476
+ final Color fg = tone == KasyAvatarTone.neutral
477
+ ? k.onSurface
478
+ : accentColor();
479
+ return _KasyAvatarColors(background: k.avatarFallbackFill, foreground: fg);
480
+ }
481
+
482
+ // Neutral soft = same as neutral solid (Figma: no tint for default type).
483
+ if (tone == KasyAvatarTone.neutral) {
432
484
  return _KasyAvatarColors(
433
485
  background: k.avatarFallbackFill,
434
- foreground: accent(),
486
+ foreground: k.onSurface,
435
487
  );
436
488
  }
437
489
 
438
- if (dark) {
439
- final Color a = accent();
440
- final Color bg = Color.alphaBlend(a.withValues(alpha: 0.28), k.surface);
441
- final Color fg = Color.lerp(a, k.onSurface, 0.45)!;
442
- return _KasyAvatarColors(background: bg, foreground: fg);
443
- }
444
-
445
- return switch (tone) {
446
- KasyAvatarTone.blue => const _KasyAvatarColors(
447
- background: Color(0xFFE3F2FD),
448
- foreground: Color(0xFF1565C0),
449
- ),
450
- KasyAvatarTone.neutral => const _KasyAvatarColors(
451
- background: Color(0xFFF0F0F0),
452
- foreground: Color(0xFF212121),
453
- ),
454
- KasyAvatarTone.green => const _KasyAvatarColors(
455
- background: Color(0xFFE8F5E9),
456
- foreground: Color(0xFF1B5E20),
457
- ),
458
- KasyAvatarTone.orange => const _KasyAvatarColors(
459
- background: Color(0xFFFFF3E0),
460
- foreground: Color(0xFFE65100),
461
- ),
462
- KasyAvatarTone.red => const _KasyAvatarColors(
463
- background: Color(0xFFFFEBEE),
464
- foreground: Color(0xFFB71C1C),
465
- ),
466
- };
490
+ // Colored soft: rgba(accent, 0.15) blended over surface (white light / dark).
491
+ // Figma fill_MVOTCC: rgba(4,133,247,0.15) + #FFFFFF light
492
+ // rgba(4,133,247,0.15) + #18181B dark
493
+ // Foreground: pure accent color (Figma uses raw accent even in soft).
494
+ final Color a = accentColor();
495
+ final Color base = dark ? k.surface : const Color(0xFFFFFFFF);
496
+ final Color bg = Color.alphaBlend(a.withValues(alpha: 0.15), base);
497
+ return _KasyAvatarColors(background: bg, foreground: a);
467
498
  }
468
499
 
469
500
  class _KasyAvatarPressable extends StatefulWidget {