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.
- package/bin/kasy.js +18 -5
- package/lib/commands/ios.js +8 -2
- package/lib/commands/reset.js +100 -2
- package/lib/commands/splash.js +11 -0
- package/lib/scaffold/backends/api/pubspec.yaml.tpl +2 -2
- package/lib/scaffold/backends/supabase/pubspec.yaml.tpl +2 -2
- package/lib/utils/apple-release.js +30 -0
- package/lib/utils/checks.js +41 -2
- package/lib/utils/debug.js +75 -0
- package/lib/utils/friendly-error.js +91 -0
- package/lib/utils/i18n/messages-en.js +970 -0
- package/lib/utils/i18n/messages-es.js +968 -0
- package/lib/utils/i18n/messages-pt.js +968 -0
- package/lib/utils/i18n.js +21 -2818
- package/lib/utils/png-padding.js +120 -0
- package/package.json +8 -3
- package/templates/firebase/android/app/src/main/kotlin/com/aicrus/firebase/kit/MyWidget.kt +12 -11
- package/templates/firebase/android/app/src/main/res/drawable-hdpi/android12splash.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-hdpi/ic_launcher_background.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-mdpi/android12splash.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-mdpi/ic_launcher_background.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-night-hdpi/android12splash.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-night-mdpi/android12splash.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-night-xhdpi/android12splash.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-night-xxhdpi/android12splash.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-night-xxxhdpi/android12splash.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-xhdpi/android12splash.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-xhdpi/ic_launcher_background.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-xxhdpi/android12splash.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-xxhdpi/ic_launcher_background.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-xxxhdpi/android12splash.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_background.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png +0 -0
- package/templates/firebase/android/app/src/main/res/layout/widget_preview.xml +18 -11
- package/templates/firebase/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +9 -0
- package/templates/firebase/assets/images/icon_android.png +0 -0
- package/templates/firebase/assets/images/icon_foreground_empty.png +0 -0
- package/templates/firebase/assets/images/splash_logo_dark_android12.png +0 -0
- package/templates/firebase/assets/images/splash_logo_light_android12.png +0 -0
- package/templates/firebase/lib/components/components.dart +1 -0
- package/templates/firebase/lib/components/kasy_avatar.dart +88 -57
- package/templates/firebase/lib/components/kasy_avatar_presets.dart +116 -74
- package/templates/firebase/lib/components/kasy_tabs.dart +431 -0
- package/templates/firebase/lib/core/home_widgets/home_widget_mywidget_service.dart +12 -6
- package/templates/firebase/lib/features/home/home_components_page.dart +1 -1
- package/templates/firebase/lib/features/home/home_components_preview_registry.dart +316 -93
- package/templates/firebase/pubspec.yaml +4 -2
- 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.
|
|
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
|
-
"
|
|
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
|
|
224
|
-
/// app icon
|
|
225
|
-
///
|
|
226
|
-
///
|
|
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
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
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
|
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/templates/firebase/android/app/src/main/res/drawable-night-xhdpi/android12splash.png
CHANGED
|
Binary file
|
package/templates/firebase/android/app/src/main/res/drawable-night-xxhdpi/android12splash.png
CHANGED
|
Binary file
|
package/templates/firebase/android/app/src/main/res/drawable-night-xxxhdpi/android12splash.png
CHANGED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/templates/firebase/android/app/src/main/res/drawable-xxhdpi/ic_launcher_background.png
ADDED
|
Binary file
|
package/templates/firebase/android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png
ADDED
|
Binary file
|
|
Binary file
|
package/templates/firebase/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_background.png
ADDED
|
Binary file
|
package/templates/firebase/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png
ADDED
|
Binary file
|
|
@@ -1,19 +1,21 @@
|
|
|
1
1
|
<?xml version="1.0" encoding="utf-8"?>
|
|
2
|
-
<!-- Static preview shown in the
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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:
|
|
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="
|
|
29
|
+
android:textSize="22sp"
|
|
28
30
|
android:textStyle="bold" />
|
|
29
31
|
|
|
30
|
-
|
|
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:
|
|
40
|
-
android:
|
|
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>
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -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
|
|
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 =
|
|
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.
|
|
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:
|
|
256
|
+
style: TextStyle(
|
|
257
257
|
color: fg,
|
|
258
|
-
fontWeight: FontWeight.
|
|
259
|
-
fontSize: _d * 0.
|
|
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
|
|
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.
|
|
338
|
-
this.showShadow =
|
|
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
|
|
350
|
-
|
|
351
|
-
|
|
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:
|
|
375
|
-
color: context.
|
|
376
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
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:
|
|
486
|
+
foreground: k.onSurface,
|
|
435
487
|
);
|
|
436
488
|
}
|
|
437
489
|
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
return
|
|
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 {
|