kasy-cli 1.32.0 → 1.35.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/README.md +1 -1
- package/bin/kasy.js +66 -2
- package/docs/cli-reference.md +7 -7
- package/lib/commands/apple-web.js +222 -0
- package/lib/commands/configure.js +3 -91
- package/lib/commands/doctor.js +20 -0
- package/lib/commands/facebook.js +189 -0
- package/lib/commands/new.js +61 -11
- package/lib/commands/release-version.js +234 -0
- package/lib/commands/update.js +27 -0
- package/lib/scaffold/CHANGELOG.json +27 -0
- package/lib/scaffold/backends/api/patch/lib/features/authentication/api/authentication_api.dart +24 -0
- package/lib/scaffold/backends/api/patch/lib/features/authentication/api/authentication_api_interface.dart +15 -0
- package/lib/scaffold/backends/api/patch/lib/features/authentication/repositories/authentication_repository.dart +27 -0
- package/lib/scaffold/backends/api/patch/lib/features/subscriptions/api/entities/subscription_entity.dart +1 -0
- package/lib/scaffold/backends/firebase/setup-from-scratch.js +199 -21
- package/lib/scaffold/backends/patch-base-hashes.json +66 -0
- package/lib/scaffold/backends/supabase/deploy.js +92 -0
- package/lib/scaffold/backends/supabase/edge-functions/stripe-webhook/index.ts +2 -0
- package/lib/scaffold/backends/supabase/migrations/20240101000007_welcome_notification.sql +36 -23
- package/lib/scaffold/backends/supabase/migrations/20240101000014_subscription_trial_end.sql +6 -0
- package/lib/scaffold/backends/supabase/patch/lib/features/authentication/api/authentication_api.dart +92 -3
- package/lib/scaffold/backends/supabase/patch/lib/features/authentication/repositories/authentication_repository.dart +36 -0
- package/lib/scaffold/backends/supabase/patch/lib/features/subscriptions/api/entities/subscription_entity.dart +1 -0
- package/lib/scaffold/generate.js +53 -4
- package/lib/scaffold/shared/generator-utils.js +18 -6
- package/lib/utils/apple-web.js +147 -0
- package/lib/utils/facebook.js +162 -0
- package/lib/utils/i18n/messages-en.js +85 -0
- package/lib/utils/i18n/messages-es.js +85 -0
- package/lib/utils/i18n/messages-pt.js +85 -0
- package/package.json +5 -2
- package/templates/firebase/.firebase/hosting.YnVpbGQvd2Vi.cache +73 -0
- package/templates/firebase/AGENTS.md +170 -0
- package/templates/firebase/CLAUDE.md +16 -0
- package/templates/firebase/DESIGN_SYSTEM.md +269 -0
- package/templates/firebase/docs/auth-setup.en.md +4 -2
- package/templates/firebase/docs/auth-setup.es.md +4 -2
- package/templates/firebase/docs/auth-setup.pt.md +4 -2
- package/templates/firebase/firebase.json +56 -1
- package/templates/firebase/functions/src/authentication/triggers.ts +10 -6
- package/templates/firebase/functions/src/core/data/entities/subscription_entity.ts +5 -0
- package/templates/firebase/functions/src/core/data/entities/user_entity.ts +9 -1
- package/templates/firebase/functions/src/core/data/repositories/user_repository.ts +27 -0
- package/templates/firebase/functions/src/subscriptions/models/subscriptions.ts +3 -0
- package/templates/firebase/functions/src/subscriptions/stripe_functions.ts +4 -0
- package/templates/firebase/lib/components/components.dart +1 -0
- package/templates/firebase/lib/components/kasy_alert.dart +0 -1
- package/templates/firebase/lib/components/kasy_app_bar.dart +35 -17
- package/templates/firebase/lib/components/kasy_bottom_sheet.dart +0 -1
- package/templates/firebase/lib/components/kasy_date_picker.dart +0 -5
- package/templates/firebase/lib/components/kasy_dialog.dart +0 -1
- package/templates/firebase/lib/components/kasy_otp_verification_bottom_sheet.dart +1 -1
- package/templates/firebase/lib/components/kasy_screen.dart +114 -0
- package/templates/firebase/lib/components/kasy_sidebar.dart +189 -120
- package/templates/firebase/lib/components/kasy_text_area.dart +0 -1
- package/templates/firebase/lib/components/kasy_text_field.dart +0 -1
- package/templates/firebase/lib/components/kasy_text_field_otp.dart +0 -1
- package/templates/firebase/lib/components/kasy_toast.dart +108 -73
- package/templates/firebase/lib/core/app_update/app_update_repository.dart +54 -0
- package/templates/firebase/lib/core/app_update/app_update_status.dart +14 -0
- package/templates/firebase/lib/core/app_update/update_available_sheet.dart +70 -0
- package/templates/firebase/lib/core/bottom_menu/active_tab_notifier.dart +40 -3
- package/templates/firebase/lib/core/bottom_menu/bottom_menu.dart +82 -34
- package/templates/firebase/lib/core/bottom_menu/web_content_wrapper.dart +6 -6
- package/templates/firebase/lib/core/bottom_menu/web_url.dart +20 -0
- package/templates/firebase/lib/core/chrome/chrome_visibility.dart +22 -0
- package/templates/firebase/lib/core/config/features.dart +5 -0
- package/templates/firebase/lib/core/data/api/remote_config_api.dart +38 -1
- package/templates/firebase/lib/core/guards/guard.dart +16 -2
- package/templates/firebase/lib/core/icons/kasy_icons.dart +3 -0
- package/templates/firebase/lib/core/rating/widgets/rate_banner.dart +2 -5
- package/templates/firebase/lib/core/rating/widgets/review_popup.dart +48 -124
- package/templates/firebase/lib/core/shared_preferences/shared_preferences.dart +14 -0
- package/templates/firebase/lib/core/states/components/maybe_show_update_available.dart +32 -0
- package/templates/firebase/lib/core/states/logout_action.dart +5 -1
- package/templates/firebase/lib/core/states/user_state_notifier.dart +85 -14
- package/templates/firebase/lib/core/theme/responsive_text_theme.dart +69 -0
- package/templates/firebase/lib/core/theme/texts.dart +90 -57
- package/templates/firebase/lib/core/theme/type_scale.dart +77 -0
- package/templates/firebase/lib/core/theme/web_background_sync_web.dart +20 -6
- package/templates/firebase/lib/core/utils/image_bytes_loader.dart +12 -0
- package/templates/firebase/lib/core/utils/image_bytes_loader_io.dart +28 -0
- package/templates/firebase/lib/core/utils/image_bytes_loader_web.dart +56 -0
- package/templates/firebase/lib/core/web_screen_width.dart +15 -0
- package/templates/firebase/lib/core/web_screen_width_io.dart +3 -0
- package/templates/firebase/lib/core/web_screen_width_web.dart +10 -0
- package/templates/firebase/lib/core/web_viewport_scale.dart +61 -25
- package/templates/firebase/lib/core/widgets/focus_visibility.dart +89 -0
- package/templates/firebase/lib/core/widgets/update_bottom_sheet.dart +29 -126
- package/templates/firebase/lib/features/ai_chat/ai_chat_page.dart +11 -8
- package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_chat_composer.dart +21 -0
- package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_chat_conversation_view.dart +1 -2
- package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_conversation_list.dart +2 -3
- package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_conversation_tile.dart +1 -2
- package/templates/firebase/lib/features/authentication/api/auth_web_support.dart +12 -0
- package/templates/firebase/lib/features/authentication/api/auth_web_support_web.dart +25 -0
- package/templates/firebase/lib/features/authentication/api/authentication_api.dart +266 -0
- package/templates/firebase/lib/features/authentication/api/authentication_api_interface.dart +22 -0
- package/templates/firebase/lib/features/authentication/navigation/post_login_navigation.dart +32 -0
- package/templates/firebase/lib/features/authentication/providers/phone_auth_notifier.dart +7 -7
- package/templates/firebase/lib/features/authentication/providers/signin_state_provider.dart +34 -10
- package/templates/firebase/lib/features/authentication/providers/signup_state_provider.dart +2 -2
- package/templates/firebase/lib/features/authentication/repositories/authentication_repository.dart +37 -0
- package/templates/firebase/lib/features/authentication/repositories/exceptions/authentication_exceptions.dart +8 -0
- package/templates/firebase/lib/features/authentication/ui/components/otp_verification.dart +2 -2
- package/templates/firebase/lib/features/authentication/ui/components/phone_input.dart +2 -2
- package/templates/firebase/lib/features/authentication/ui/signin_page.dart +80 -15
- package/templates/firebase/lib/features/authentication/ui/signup_page.dart +20 -14
- package/templates/firebase/lib/features/authentication/ui/widgets/auth_card_scaffold.dart +2 -2
- package/templates/firebase/lib/features/authentication/ui/widgets/recover_password_result.dart +2 -2
- package/templates/firebase/lib/features/feedbacks/ui/widgets/add_feature_button.dart +0 -1
- package/templates/firebase/lib/features/feedbacks/ui/widgets/feature_card.dart +1 -2
- package/templates/firebase/lib/features/home/design_system_page.dart +134 -67
- package/templates/firebase/lib/features/home/home_components_page.dart +8 -1
- package/templates/firebase/lib/features/home/home_components_preview_page.dart +1 -3
- package/templates/firebase/lib/features/home/home_components_preview_registry.dart +186 -56
- package/templates/firebase/lib/features/home/home_page.dart +4 -0
- package/templates/firebase/lib/features/local_reminders/ui/reminder_page.dart +169 -208
- package/templates/firebase/lib/features/notifications/providers/notifications_provider.dart +10 -0
- package/templates/firebase/lib/features/notifications/ui/components/notification_settings_sheet.dart +2 -2
- package/templates/firebase/lib/features/notifications/ui/components/notification_tile.dart +21 -8
- package/templates/firebase/lib/features/notifications/ui/components/push_notification_switcher.dart +2 -2
- package/templates/firebase/lib/features/notifications/ui/notifications_page.dart +3 -4
- package/templates/firebase/lib/features/notifications/ui/widgets/notification_tile.dart +84 -128
- package/templates/firebase/lib/features/onboarding/providers/onboarding_model.dart +11 -0
- package/templates/firebase/lib/features/onboarding/providers/onboarding_provider.dart +30 -3
- package/templates/firebase/lib/features/onboarding/ui/components/onboarding_loader.dart +13 -4
- package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_illustration_scaffold.dart +3 -4
- package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_radio_question.dart +2 -4
- package/templates/firebase/lib/features/onboarding/ui/widgets/selectable_row_tile.dart +2 -1
- package/templates/firebase/lib/features/settings/settings_page.dart +152 -11
- package/templates/firebase/lib/features/settings/ui/components/admin/admin_page.dart +58 -21
- package/templates/firebase/lib/features/settings/ui/components/admin/admin_users_tab.dart +12 -12
- package/templates/firebase/lib/features/settings/ui/components/admin/send_push_notification_page.dart +1 -4
- package/templates/firebase/lib/features/settings/ui/components/avatar_component.dart +2 -2
- package/templates/firebase/lib/features/settings/ui/components/create_password_sheet.dart +141 -0
- package/templates/firebase/lib/features/settings/ui/components/language_switcher.dart +0 -1
- package/templates/firebase/lib/features/settings/ui/widgets/admin_card.dart +42 -38
- package/templates/firebase/lib/features/settings/ui/widgets/settings_tile.dart +17 -2
- package/templates/firebase/lib/features/subscriptions/api/entities/subscription_entity.dart +3 -0
- package/templates/firebase/lib/features/subscriptions/api/stripe_product.dart +12 -11
- package/templates/firebase/lib/features/subscriptions/repositories/subscription_repository.dart +25 -2
- package/templates/firebase/lib/features/subscriptions/ui/component/active_premium_content.dart +319 -143
- package/templates/firebase/lib/features/subscriptions/ui/component/paywall_minimal.dart +1 -5
- package/templates/firebase/lib/features/subscriptions/ui/component/paywall_row.dart +1 -5
- package/templates/firebase/lib/features/subscriptions/ui/component/paywall_with_switch.dart +2 -5
- package/templates/firebase/lib/features/subscriptions/ui/component/premium_content.dart +1 -4
- package/templates/firebase/lib/features/subscriptions/ui/widgets/feature_line.dart +1 -2
- package/templates/firebase/lib/features/subscriptions/ui/widgets/premium_banner.dart +2 -7
- package/templates/firebase/lib/features/subscriptions/ui/widgets/premium_feature.dart +2 -4
- package/templates/firebase/lib/features/subscriptions/ui/widgets/selectable_col.dart +1 -4
- package/templates/firebase/lib/features/subscriptions/ui/widgets/selectable_row.dart +0 -3
- package/templates/firebase/lib/i18n/en.i18n.json +54 -7
- package/templates/firebase/lib/i18n/es.i18n.json +54 -7
- package/templates/firebase/lib/i18n/pt.i18n.json +54 -7
- package/templates/firebase/lib/main.dart +11 -2
- package/templates/firebase/lib/router.dart +94 -13
- package/templates/firebase/pubspec.yaml +1 -1
- package/templates/firebase/test/core/data/api/fake_remote_config_api.dart +14 -0
- package/templates/firebase/test/core/states/user_state_notifier_test.dart +47 -3
- package/templates/firebase/test/core/web_viewport_scale_test.dart +68 -0
- package/templates/firebase/test/features/authentication/data/api/auth_api_fake.dart +15 -0
- package/templates/firebase/tool/design_check.dart +152 -0
- package/templates/firebase/web/index.html +162 -14
- package/templates/firebase/assets/images/review.png +0 -0
- package/templates/firebase/assets/images/update.png +0 -0
- package/templates/firebase/lib/core/guards/user_info_guard.dart +0 -61
- package/templates/firebase/lib/features/notifications/ui/components/notifications_header.dart +0 -32
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import 'package:flutter/material.dart';
|
|
2
|
+
import 'package:kasy_kit/core/theme/texts.dart';
|
|
3
|
+
import 'package:kasy_kit/core/widgets/responsive_layout.dart';
|
|
4
|
+
|
|
5
|
+
/// Republishes the app's typography for the current breakpoint — without
|
|
6
|
+
/// touching a single call site.
|
|
7
|
+
///
|
|
8
|
+
/// It reads the effective viewport width, picks the [DeviceType], and rebuilds
|
|
9
|
+
/// the Material `textTheme` and the [KasyTextTheme] extension from the
|
|
10
|
+
/// per-breakpoint [KasyTypeScale] ramp. Headings step down from desktop to
|
|
11
|
+
/// mobile; body and labels stay constant. Colours are kept from the base theme
|
|
12
|
+
/// (the ramp is colourless), so every `context.textTheme.*` and
|
|
13
|
+
/// `context.kasyTextTheme.*` below inherits the breakpoint's sizes.
|
|
14
|
+
///
|
|
15
|
+
/// Placed as the innermost wrapper of the app (inside `WebViewportScale` and
|
|
16
|
+
/// `DevicePreview`), so it reads the *same* width the screens lay out against:
|
|
17
|
+
/// the type bucket always matches the layout bucket. Desktop is the authored
|
|
18
|
+
/// baseline, so it is a pass-through there.
|
|
19
|
+
class ResponsiveTextTheme extends StatelessWidget {
|
|
20
|
+
final Widget child;
|
|
21
|
+
|
|
22
|
+
const ResponsiveTextTheme({super.key, required this.child});
|
|
23
|
+
|
|
24
|
+
@override
|
|
25
|
+
Widget build(BuildContext context) {
|
|
26
|
+
final DeviceType device =
|
|
27
|
+
DeviceType.fromWidth(MediaQuery.sizeOf(context).width);
|
|
28
|
+
|
|
29
|
+
// Desktop is the authored baseline (the base theme built in main.dart) — no
|
|
30
|
+
// rebuild needed.
|
|
31
|
+
if (device != DeviceType.small && device != DeviceType.medium) {
|
|
32
|
+
return child;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
final ThemeData theme = Theme.of(context);
|
|
36
|
+
final KasyTextTheme ramp = KasyTextTheme.build(device);
|
|
37
|
+
final TextTheme base = theme.textTheme;
|
|
38
|
+
|
|
39
|
+
// Take size/weight/line-height from the ramp, keep colour from the base
|
|
40
|
+
// (the ramp is colourless, so merge preserves the base slot's colour).
|
|
41
|
+
final TextTheme rebuilt = TextTheme(
|
|
42
|
+
displayLarge: base.displayLarge?.merge(ramp.displayLarge),
|
|
43
|
+
displayMedium: base.displayMedium?.merge(ramp.displayMedium),
|
|
44
|
+
displaySmall: base.displaySmall?.merge(ramp.displaySmall),
|
|
45
|
+
headlineLarge: base.headlineLarge?.merge(ramp.headlineLarge),
|
|
46
|
+
headlineMedium: base.headlineMedium?.merge(ramp.headlineMedium),
|
|
47
|
+
headlineSmall: base.headlineSmall?.merge(ramp.headlineSmall),
|
|
48
|
+
titleLarge: base.titleLarge?.merge(ramp.titleLarge),
|
|
49
|
+
titleMedium: base.titleMedium?.merge(ramp.titleMedium),
|
|
50
|
+
titleSmall: base.titleSmall?.merge(ramp.titleSmall),
|
|
51
|
+
bodyLarge: base.bodyLarge?.merge(ramp.bodyLarge),
|
|
52
|
+
bodyMedium: base.bodyMedium?.merge(ramp.bodyMedium),
|
|
53
|
+
bodySmall: base.bodySmall?.merge(ramp.bodySmall),
|
|
54
|
+
labelLarge: base.labelLarge?.merge(ramp.labelLarge),
|
|
55
|
+
labelMedium: base.labelMedium?.merge(ramp.labelMedium),
|
|
56
|
+
labelSmall: base.labelSmall?.merge(ramp.labelSmall),
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
// Replace only the KasyTextTheme extension; keep every other extension.
|
|
60
|
+
final List<ThemeExtension<dynamic>> extensions = theme.extensions.values
|
|
61
|
+
.map((e) => e is KasyTextTheme ? ramp : e)
|
|
62
|
+
.toList();
|
|
63
|
+
|
|
64
|
+
return Theme(
|
|
65
|
+
data: theme.copyWith(textTheme: rebuilt, extensions: extensions),
|
|
66
|
+
child: child,
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import 'package:flutter/material.dart';
|
|
2
2
|
import 'package:google_fonts/google_fonts.dart';
|
|
3
|
+
import 'package:kasy_kit/core/theme/type_scale.dart';
|
|
4
|
+
import 'package:kasy_kit/core/widgets/responsive_layout.dart';
|
|
3
5
|
|
|
4
6
|
/// Kasy Design System — Typography Tokens (HeroUI type scale)
|
|
5
7
|
///
|
|
@@ -32,6 +34,22 @@ class KasyTextTheme extends ThemeExtension<KasyTextTheme> {
|
|
|
32
34
|
decoration: decoration,
|
|
33
35
|
);
|
|
34
36
|
|
|
37
|
+
// Builds a role from its responsive [RampSize] at a given [device]: the size
|
|
38
|
+
// (and proportional line-height) come from the breakpoint, the weight from
|
|
39
|
+
// the role. This is how every slot picks up the per-breakpoint type scale.
|
|
40
|
+
static TextStyle _fromRamp(
|
|
41
|
+
FontWeight weight,
|
|
42
|
+
RampSize ramp,
|
|
43
|
+
DeviceType device, {
|
|
44
|
+
TextDecoration? decoration,
|
|
45
|
+
}) =>
|
|
46
|
+
_inter(
|
|
47
|
+
weight,
|
|
48
|
+
ramp.size(device),
|
|
49
|
+
ramp.lineHeight(device),
|
|
50
|
+
decoration: decoration,
|
|
51
|
+
);
|
|
52
|
+
|
|
35
53
|
// -----------------------------------------------------------------------
|
|
36
54
|
// HeroUI roles — canonical styles (color applied per-theme downstream)
|
|
37
55
|
// -----------------------------------------------------------------------
|
|
@@ -54,37 +72,9 @@ class KasyTextTheme extends ThemeExtension<KasyTextTheme> {
|
|
|
54
72
|
static TextStyle get buttonBase => _inter(FontWeight.w500, 16, 24);
|
|
55
73
|
static TextStyle get buttonSm => _inter(FontWeight.w500, 14, 20);
|
|
56
74
|
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
//
|
|
60
|
-
// scale role above; restyle a role app-wide by editing it here once.
|
|
61
|
-
// Apply color at the call site (.copyWith(color: ...)); the scale is colorless.
|
|
62
|
-
// -----------------------------------------------------------------------
|
|
63
|
-
|
|
64
|
-
/// Top-level page title (auth, paywall, full-screen flows). 24 / w700.
|
|
65
|
-
static TextStyle get pageTitle => heading2;
|
|
66
|
-
|
|
67
|
-
/// Section or master-detail pane title (Settings detail, grouped areas). 16 / w600.
|
|
68
|
-
static TextStyle get sectionTitle => heading4;
|
|
69
|
-
|
|
70
|
-
/// Small header above a grouped list (e.g. "PREFERENCES"). 12 / w600, lightly tracked.
|
|
71
|
-
static TextStyle get sectionLabel =>
|
|
72
|
-
_inter(FontWeight.w600, 12, 16).copyWith(letterSpacing: 0.5);
|
|
73
|
-
|
|
74
|
-
/// Primary text of a list / settings row. 14 / w500.
|
|
75
|
-
static TextStyle get rowTitle => bodySmMedium;
|
|
76
|
-
|
|
77
|
-
/// Secondary / value text in a row (apply a muted color at the call site). 14 / w400.
|
|
78
|
-
static TextStyle get rowValue => bodySm;
|
|
79
|
-
|
|
80
|
-
/// Card title. 14 / w500.
|
|
81
|
-
static TextStyle get cardTitle => bodySmMedium;
|
|
82
|
-
|
|
83
|
-
/// Card subtitle / supporting line. 12 / w400.
|
|
84
|
-
static TextStyle get cardSubtitle => bodyXs;
|
|
85
|
-
|
|
86
|
-
/// Caption, hint, version label, footnote. 12 / w400.
|
|
87
|
-
static TextStyle get caption => bodyXs;
|
|
75
|
+
// Semantic UI roles (pageTitle, sectionTitle, rowTitle, …) live as INSTANCE
|
|
76
|
+
// getters further down, so they pick up the per-breakpoint scaling. The raw
|
|
77
|
+
// scale roles above stay static (the authored spec / design-system baseline).
|
|
88
78
|
|
|
89
79
|
// -----------------------------------------------------------------------
|
|
90
80
|
// Display — large hero text (extends the HeroUI scale upward in Inter)
|
|
@@ -143,35 +133,78 @@ class KasyTextTheme extends ThemeExtension<KasyTextTheme> {
|
|
|
143
133
|
required this.primary,
|
|
144
134
|
});
|
|
145
135
|
|
|
146
|
-
|
|
136
|
+
// -----------------------------------------------------------------------
|
|
137
|
+
// Semantic app roles — INSTANCE getters (responsive).
|
|
138
|
+
//
|
|
139
|
+
// These mirror the static roles above, but derive from this instance's
|
|
140
|
+
// fields, so they pick up the per-breakpoint scaling applied by
|
|
141
|
+
// [ResponsiveTextTheme]. Prefer `context.kasyTextTheme.<role>` on screens
|
|
142
|
+
// when you need a role that scales (the static `KasyTextTheme.<role>` stays
|
|
143
|
+
// fixed at the desktop baseline — fine for specs/docs, not for live UI).
|
|
144
|
+
// Apply colour at the call site; the scale is colourless.
|
|
145
|
+
// -----------------------------------------------------------------------
|
|
146
|
+
|
|
147
|
+
/// Top-level page title (auth, paywall, full-screen flows). 24 / w700.
|
|
148
|
+
TextStyle get pageTitle => headlineMedium;
|
|
149
|
+
|
|
150
|
+
/// Section or master-detail pane title (Settings detail, grouped areas). 16 / w600.
|
|
151
|
+
TextStyle get sectionTitle => titleMedium;
|
|
152
|
+
|
|
153
|
+
/// Small header above a grouped list (e.g. "PREFERENCES"). 12 / w600, lightly tracked.
|
|
154
|
+
TextStyle get sectionLabel =>
|
|
155
|
+
bodySmall.copyWith(fontWeight: FontWeight.w600, letterSpacing: 0.5);
|
|
156
|
+
|
|
157
|
+
/// Primary text of a list / settings row. 14 / w500.
|
|
158
|
+
TextStyle get rowTitle => titleSmall;
|
|
159
|
+
|
|
160
|
+
/// Secondary / value text in a row (apply a muted colour at the call site). 14 / w400.
|
|
161
|
+
TextStyle get rowValue => bodyMedium;
|
|
162
|
+
|
|
163
|
+
/// Card title. 14 / w500.
|
|
164
|
+
TextStyle get cardTitle => titleSmall;
|
|
165
|
+
|
|
166
|
+
/// Card subtitle / supporting line. 12 / w400.
|
|
167
|
+
TextStyle get cardSubtitle => bodySmall;
|
|
168
|
+
|
|
169
|
+
/// Caption, hint, version label, footnote. 12 / w400.
|
|
170
|
+
TextStyle get caption => bodySmall;
|
|
171
|
+
|
|
172
|
+
/// Builds the full text theme for a given [device].
|
|
173
|
+
///
|
|
174
|
+
/// Each slot resolves its size from the responsive [KasyTypeScale] ramp at the
|
|
175
|
+
/// breakpoint: headings step down from desktop to mobile, body and labels stay
|
|
176
|
+
/// constant. Defaults to desktop ([DeviceType.large]) so the base theme built
|
|
177
|
+
/// once in `main.dart` is the authored reference; [ResponsiveTextTheme]
|
|
178
|
+
/// rebuilds it per breakpoint below the MediaQuery.
|
|
179
|
+
factory KasyTextTheme.build([DeviceType device = DeviceType.large]) {
|
|
147
180
|
return KasyTextTheme(
|
|
148
|
-
// Display — hero text
|
|
149
|
-
displayLarge:
|
|
150
|
-
displayMedium:
|
|
151
|
-
displaySmall: heading1,
|
|
152
|
-
|
|
153
|
-
// Headline →
|
|
154
|
-
headlineLarge: heading1,
|
|
155
|
-
headlineMedium: heading2,
|
|
156
|
-
headlineSmall: heading3,
|
|
157
|
-
|
|
158
|
-
// Title →
|
|
159
|
-
titleLarge: heading3,
|
|
160
|
-
titleMedium: heading4,
|
|
161
|
-
titleSmall:
|
|
162
|
-
|
|
163
|
-
// Body →
|
|
164
|
-
bodyLarge: bodyBase,
|
|
165
|
-
bodyMedium: bodySm,
|
|
166
|
-
bodySmall: bodyXs,
|
|
167
|
-
|
|
168
|
-
// Label →
|
|
169
|
-
labelLarge:
|
|
170
|
-
labelMedium:
|
|
171
|
-
labelSmall:
|
|
181
|
+
// Display — hero text.
|
|
182
|
+
displayLarge: _fromRamp(FontWeight.w800, KasyTypeScale.displayLarge, device),
|
|
183
|
+
displayMedium: _fromRamp(FontWeight.w800, KasyTypeScale.displayMedium, device),
|
|
184
|
+
displaySmall: _fromRamp(FontWeight.w800, KasyTypeScale.heading1, device),
|
|
185
|
+
|
|
186
|
+
// Headline → Heading 1–3.
|
|
187
|
+
headlineLarge: _fromRamp(FontWeight.w800, KasyTypeScale.heading1, device),
|
|
188
|
+
headlineMedium: _fromRamp(FontWeight.w700, KasyTypeScale.heading2, device),
|
|
189
|
+
headlineSmall: _fromRamp(FontWeight.w600, KasyTypeScale.heading3, device),
|
|
190
|
+
|
|
191
|
+
// Title → Heading 3 / Heading 4 / Body sm medium.
|
|
192
|
+
titleLarge: _fromRamp(FontWeight.w600, KasyTypeScale.heading3, device),
|
|
193
|
+
titleMedium: _fromRamp(FontWeight.w600, KasyTypeScale.heading4, device),
|
|
194
|
+
titleSmall: _fromRamp(FontWeight.w500, KasyTypeScale.bodySm, device),
|
|
195
|
+
|
|
196
|
+
// Body → Body base / Body sm / Body xs.
|
|
197
|
+
bodyLarge: _fromRamp(FontWeight.w400, KasyTypeScale.bodyBase, device),
|
|
198
|
+
bodyMedium: _fromRamp(FontWeight.w400, KasyTypeScale.bodySm, device),
|
|
199
|
+
bodySmall: _fromRamp(FontWeight.w400, KasyTypeScale.bodyXs, device),
|
|
200
|
+
|
|
201
|
+
// Label → Button sm / Body xs medium.
|
|
202
|
+
labelLarge: _fromRamp(FontWeight.w500, KasyTypeScale.bodySm, device),
|
|
203
|
+
labelMedium: _fromRamp(FontWeight.w500, KasyTypeScale.bodyXs, device),
|
|
204
|
+
labelSmall: _fromRamp(FontWeight.w500, KasyTypeScale.bodyXs, device),
|
|
172
205
|
|
|
173
206
|
// Legacy primary.
|
|
174
|
-
primary: bodyBase,
|
|
207
|
+
primary: _fromRamp(FontWeight.w400, KasyTypeScale.bodyBase, device),
|
|
175
208
|
);
|
|
176
209
|
}
|
|
177
210
|
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import 'package:kasy_kit/core/widgets/responsive_layout.dart';
|
|
2
|
+
|
|
3
|
+
/// Responsive type scale — explicit font size per role, per breakpoint.
|
|
4
|
+
///
|
|
5
|
+
/// Single source of truth for typography sizes. Instead of a blind global
|
|
6
|
+
/// multiplier, every role declares its own size at each breakpoint — the way a
|
|
7
|
+
/// design tool (Figma variables / modes) or a fluid web scale does it.
|
|
8
|
+
///
|
|
9
|
+
/// The professional pattern this follows: **headings are largest on desktop and
|
|
10
|
+
/// compress on smaller viewports** (a 36px title would feel huge and wrap badly
|
|
11
|
+
/// on a phone), while **body and label text stay constant** for stable,
|
|
12
|
+
/// comfortable reading on every device. Desktop is the authored reference;
|
|
13
|
+
/// tablet and mobile step the headings down.
|
|
14
|
+
///
|
|
15
|
+
/// To re-tune the whole app's typography, edit the numbers here — nothing else.
|
|
16
|
+
/// The live ramp is visible under Design System -> Typography (tabs per
|
|
17
|
+
/// breakpoint).
|
|
18
|
+
class RampSize {
|
|
19
|
+
/// Phone (< 768).
|
|
20
|
+
final double mobile;
|
|
21
|
+
|
|
22
|
+
/// Tablet (768-1024).
|
|
23
|
+
final double tablet;
|
|
24
|
+
|
|
25
|
+
/// Desktop (>= 1024) — the authored reference.
|
|
26
|
+
final double desktop;
|
|
27
|
+
|
|
28
|
+
/// line-height / font-size. Kept constant across breakpoints so the vertical
|
|
29
|
+
/// rhythm scales together with the font size.
|
|
30
|
+
final double lineHeightRatio;
|
|
31
|
+
|
|
32
|
+
const RampSize({
|
|
33
|
+
required this.mobile,
|
|
34
|
+
required this.tablet,
|
|
35
|
+
required this.desktop,
|
|
36
|
+
required this.lineHeightRatio,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
/// The font size (logical px) for [device].
|
|
40
|
+
double size(DeviceType device) => switch (device) {
|
|
41
|
+
DeviceType.small => mobile,
|
|
42
|
+
DeviceType.medium => tablet,
|
|
43
|
+
DeviceType.large || DeviceType.xlarge => desktop,
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/// The line height (logical px) for [device].
|
|
47
|
+
double lineHeight(DeviceType device) => size(device) * lineHeightRatio;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/// The Kasy type scale: every typographic role's size across the 3 breakpoints.
|
|
51
|
+
class KasyTypeScale {
|
|
52
|
+
const KasyTypeScale._();
|
|
53
|
+
|
|
54
|
+
// Hero / display — large, compress hard on the way down.
|
|
55
|
+
static const displayLarge =
|
|
56
|
+
RampSize(mobile: 40, tablet: 48, desktop: 57, lineHeightRatio: 64 / 57);
|
|
57
|
+
static const displayMedium =
|
|
58
|
+
RampSize(mobile: 34, tablet: 40, desktop: 45, lineHeightRatio: 52 / 45);
|
|
59
|
+
|
|
60
|
+
// Headings — largest on desktop, step down on tablet then mobile.
|
|
61
|
+
static const heading1 =
|
|
62
|
+
RampSize(mobile: 28, tablet: 32, desktop: 36, lineHeightRatio: 40 / 36);
|
|
63
|
+
static const heading2 =
|
|
64
|
+
RampSize(mobile: 22, tablet: 23, desktop: 24, lineHeightRatio: 32 / 24);
|
|
65
|
+
static const heading3 =
|
|
66
|
+
RampSize(mobile: 18, tablet: 19, desktop: 20, lineHeightRatio: 28 / 20);
|
|
67
|
+
static const heading4 =
|
|
68
|
+
RampSize(mobile: 16, tablet: 16, desktop: 16, lineHeightRatio: 24 / 16);
|
|
69
|
+
|
|
70
|
+
// Body & labels — constant across breakpoints for stable, comfortable reading.
|
|
71
|
+
static const bodyBase =
|
|
72
|
+
RampSize(mobile: 16, tablet: 16, desktop: 16, lineHeightRatio: 24 / 16);
|
|
73
|
+
static const bodySm =
|
|
74
|
+
RampSize(mobile: 14, tablet: 14, desktop: 14, lineHeightRatio: 20 / 14);
|
|
75
|
+
static const bodyXs =
|
|
76
|
+
RampSize(mobile: 12, tablet: 12, desktop: 12, lineHeightRatio: 16 / 12);
|
|
77
|
+
}
|
|
@@ -2,17 +2,31 @@ import 'package:flutter/material.dart';
|
|
|
2
2
|
import 'package:web/web.dart' as web;
|
|
3
3
|
|
|
4
4
|
void syncWebBackgroundColor(Brightness brightness) {
|
|
5
|
-
final
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
5
|
+
final bool dark = brightness == Brightness.dark;
|
|
6
|
+
// iOS Safari tints its status bar / address bar with the page's background
|
|
7
|
+
// colour once the Flutter canvas covers the viewport (it ignores the
|
|
8
|
+
// theme-color meta there). It samples whichever element fills the top of the
|
|
9
|
+
// viewport — the flutter-view host, not just <body> — so we paint <html>,
|
|
10
|
+
// <body> AND the Flutter host chain with `surface` (the app bar colour). That
|
|
11
|
+
// way the Safari bars match the app bar with no seam, no matter which element
|
|
12
|
+
// Safari reads. The app's own canvas draws over these, so the page background
|
|
13
|
+
// colour only ever shows in the browser chrome.
|
|
14
|
+
final String chromeColor = dark ? '#18181B' : '#FFFFFF';
|
|
15
|
+
|
|
16
|
+
// Keep the <html data-theme> attribute (set pre-boot by the index.html script)
|
|
17
|
+
// in sync, so a later reload / hot restart paints the splash + Safari chrome
|
|
18
|
+
// with the theme currently chosen in the app.
|
|
19
|
+
final html = web.document.documentElement as web.HTMLElement?;
|
|
20
|
+
html?.setAttribute('data-theme', dark ? 'dark' : 'light');
|
|
21
|
+
html?.style.setProperty('background-color', chromeColor);
|
|
22
|
+
web.document.body?.style.setProperty('background-color', chromeColor);
|
|
23
|
+
|
|
10
24
|
final views = web.document.querySelectorAll(
|
|
11
25
|
'flutter-view, flt-glass-pane, flt-scene-host',
|
|
12
26
|
);
|
|
13
27
|
for (var i = 0; i < views.length; i++) {
|
|
14
28
|
(views.item(i) as web.HTMLElement?)
|
|
15
29
|
?.style
|
|
16
|
-
.setProperty('background-color',
|
|
30
|
+
.setProperty('background-color', chromeColor);
|
|
17
31
|
}
|
|
18
32
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/// Loads raw image bytes from a remote URL, choosing a platform-specific
|
|
2
|
+
/// strategy so social-profile photos (e.g. Google) import reliably:
|
|
3
|
+
/// - native: a direct HTTP download (see `image_bytes_loader_io.dart`),
|
|
4
|
+
/// - web: an `<img>` + canvas read that avoids the CDN throttling that hits
|
|
5
|
+
/// programmatic `fetch()`/XHR (see `image_bytes_loader_web.dart`).
|
|
6
|
+
///
|
|
7
|
+
/// Both implementations expose the same `loadImageBytes(url)` entry point and
|
|
8
|
+
/// return null (or rethrow) on failure so the caller can keep its fallback.
|
|
9
|
+
library;
|
|
10
|
+
|
|
11
|
+
export 'image_bytes_loader_io.dart'
|
|
12
|
+
if (dart.library.js_interop) 'image_bytes_loader_web.dart';
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import 'package:dio/dio.dart';
|
|
2
|
+
|
|
3
|
+
/// Downloads image bytes on native platforms.
|
|
4
|
+
///
|
|
5
|
+
/// Native has no browser fetch throttling, so a direct request with a short
|
|
6
|
+
/// retry on transient errors (HTTP 429/503, timeouts) is reliable. Returns the
|
|
7
|
+
/// raw bytes, or rethrows after exhausting retries so the caller can fall back.
|
|
8
|
+
Future<List<int>?> loadImageBytes(String url) async {
|
|
9
|
+
const delays = [Duration(milliseconds: 600), Duration(seconds: 2)];
|
|
10
|
+
for (var attempt = 0; ; attempt++) {
|
|
11
|
+
try {
|
|
12
|
+
final response = await Dio().get<List<int>>(
|
|
13
|
+
url,
|
|
14
|
+
options: Options(responseType: ResponseType.bytes),
|
|
15
|
+
);
|
|
16
|
+
return response.data;
|
|
17
|
+
} on DioException catch (e) {
|
|
18
|
+
final status = e.response?.statusCode;
|
|
19
|
+
final transient = status == 429 ||
|
|
20
|
+
status == 503 ||
|
|
21
|
+
e.type == DioExceptionType.connectionTimeout ||
|
|
22
|
+
e.type == DioExceptionType.receiveTimeout ||
|
|
23
|
+
e.type == DioExceptionType.connectionError;
|
|
24
|
+
if (!transient || attempt >= delays.length) rethrow;
|
|
25
|
+
await Future<void>.delayed(delays[attempt]);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import 'dart:async';
|
|
2
|
+
import 'dart:js_interop';
|
|
3
|
+
|
|
4
|
+
import 'package:web/web.dart' as web;
|
|
5
|
+
|
|
6
|
+
/// Downloads image bytes on web by loading the URL through an `<img>` element
|
|
7
|
+
/// and reading it back from a canvas.
|
|
8
|
+
///
|
|
9
|
+
/// Browsers serve `<img>` requests reliably, while some CDNs (notably Google's
|
|
10
|
+
/// `lh3.googleusercontent.com`) throttle programmatic `fetch()`/XHR with HTTP
|
|
11
|
+
/// 429 - which is what an HTTP client like dio uses. The image is requested with
|
|
12
|
+
/// CORS (`crossOrigin = 'anonymous'`) so the canvas is not tainted and its
|
|
13
|
+
/// pixels can be exported. Returns null on any failure, so the caller keeps its
|
|
14
|
+
/// fallback avatar.
|
|
15
|
+
Future<List<int>?> loadImageBytes(String url) async {
|
|
16
|
+
final image = web.HTMLImageElement()..crossOrigin = 'anonymous';
|
|
17
|
+
final loaded = Completer<bool>();
|
|
18
|
+
image.addEventListener(
|
|
19
|
+
'load',
|
|
20
|
+
(web.Event _) {
|
|
21
|
+
if (!loaded.isCompleted) loaded.complete(true);
|
|
22
|
+
}.toJS,
|
|
23
|
+
);
|
|
24
|
+
image.addEventListener(
|
|
25
|
+
'error',
|
|
26
|
+
(web.Event _) {
|
|
27
|
+
if (!loaded.isCompleted) loaded.complete(false);
|
|
28
|
+
}.toJS,
|
|
29
|
+
);
|
|
30
|
+
image.src = url;
|
|
31
|
+
|
|
32
|
+
final ok = await loaded.future
|
|
33
|
+
.timeout(const Duration(seconds: 10), onTimeout: () => false);
|
|
34
|
+
if (!ok || image.naturalWidth == 0) return null;
|
|
35
|
+
|
|
36
|
+
final canvas = web.HTMLCanvasElement()
|
|
37
|
+
..width = image.naturalWidth
|
|
38
|
+
..height = image.naturalHeight;
|
|
39
|
+
final context = canvas.getContext('2d') as web.CanvasRenderingContext2D?;
|
|
40
|
+
if (context == null) return null;
|
|
41
|
+
context.drawImage(image, 0, 0);
|
|
42
|
+
|
|
43
|
+
final blobReady = Completer<web.Blob?>();
|
|
44
|
+
canvas.toBlob(
|
|
45
|
+
(web.Blob? blob) {
|
|
46
|
+
blobReady.complete(blob);
|
|
47
|
+
}.toJS,
|
|
48
|
+
'image/jpeg',
|
|
49
|
+
0.92.toJS,
|
|
50
|
+
);
|
|
51
|
+
final blob = await blobReady.future;
|
|
52
|
+
if (blob == null) return null;
|
|
53
|
+
|
|
54
|
+
final buffer = await blob.arrayBuffer().toDart;
|
|
55
|
+
return buffer.toDart.asUint8List();
|
|
56
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/// Reads the physical screen width (logical/CSS px) so the desktop web scale can
|
|
2
|
+
/// compensate for high OS display scaling WITHOUT reacting to mere window resize.
|
|
3
|
+
///
|
|
4
|
+
/// The browser window width (what the layout uses) changes whenever the user
|
|
5
|
+
/// drags the window narrower — using it to decide the scale made the whole UI
|
|
6
|
+
/// shrink unnecessarily. The *screen* width, on the other hand, only shrinks when
|
|
7
|
+
/// the OS display scale goes up (Windows 150%, Mac scaled modes), which is exactly
|
|
8
|
+
/// when the compensation is actually wanted.
|
|
9
|
+
///
|
|
10
|
+
/// Both implementations expose `currentScreenWidth()`; it returns null off the web
|
|
11
|
+
/// (where the scaling path is never reached anyway).
|
|
12
|
+
library;
|
|
13
|
+
|
|
14
|
+
export 'web_screen_width_io.dart'
|
|
15
|
+
if (dart.library.js_interop) 'web_screen_width_web.dart';
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import 'package:web/web.dart' as web;
|
|
2
|
+
|
|
3
|
+
/// The screen width in logical (CSS) px. This shrinks when the OS display scale
|
|
4
|
+
/// goes up (Windows 150%, Mac scaled modes) but does NOT change when the user
|
|
5
|
+
/// merely resizes the browser window — exactly the signal the desktop scale
|
|
6
|
+
/// compensation needs. Returns null if the value is unavailable (0/invalid).
|
|
7
|
+
double? currentScreenWidth() {
|
|
8
|
+
final int width = web.window.screen.width;
|
|
9
|
+
return width > 0 ? width.toDouble() : null;
|
|
10
|
+
}
|
|
@@ -1,32 +1,73 @@
|
|
|
1
1
|
import 'package:flutter/foundation.dart' show kIsWeb;
|
|
2
2
|
import 'package:flutter/widgets.dart';
|
|
3
|
+
import 'package:kasy_kit/core/web_screen_width.dart';
|
|
3
4
|
|
|
4
|
-
/// Maximum render scale applied to the app on web (
|
|
5
|
+
/// Maximum render scale applied to the app on web (desktop breakpoint only).
|
|
5
6
|
///
|
|
6
7
|
/// Flutter web tends to render ~10% larger than equivalent HTML apps at the
|
|
7
8
|
/// browser's 100% zoom, so the whole UI feels oversized on desktop. `0.95`
|
|
8
9
|
/// brings it to the proportion the design targets (i.e. what 95% zoom looked
|
|
9
|
-
/// like) without the user having to touch the browser zoom.
|
|
10
|
-
///
|
|
10
|
+
/// like) without the user having to touch the browser zoom. It acts as the cap:
|
|
11
|
+
/// a desktop whose logical width is below the design target (high OS scale)
|
|
12
|
+
/// reduces it further to pin the layout (see [kWebViewportScaleTargetWidth]).
|
|
13
|
+
///
|
|
14
|
+
/// Mobile and tablet web are deliberately left at 1.0 — same as native and as the
|
|
15
|
+
/// device preview, so a developer previewing a phone/tablet sees exactly what the
|
|
16
|
+
/// native build renders. NATIVE is never scaled: the mechanism is gated on
|
|
17
|
+
/// [kIsWeb], so iOS/Android/macOS/Windows apps render at 1.0 and keep respecting
|
|
18
|
+
/// the user's system text-size (accessibility).
|
|
11
19
|
const double kWebViewportScale = 0.95;
|
|
12
20
|
|
|
13
21
|
/// Design target width (logical px) the desktop shell is laid out against.
|
|
14
22
|
///
|
|
15
|
-
/// A
|
|
16
|
-
///
|
|
17
|
-
///
|
|
18
|
-
///
|
|
19
|
-
/// `
|
|
20
|
-
///
|
|
23
|
+
/// A display with high OS scaling (Windows at 125/150/175%, or a Mac in a scaled
|
|
24
|
+
/// "more space"/"larger text" mode) reports a smaller logical SCREEN width, so the
|
|
25
|
+
/// `0.95` baseline alone left the shell cramped/cropped (the user had to
|
|
26
|
+
/// Ctrl-minus). When the screen is below this target the scale drops further
|
|
27
|
+
/// (`screenWidth / kWebViewportScaleTargetWidth`) so the full design still fits.
|
|
28
|
+
/// Compared against the SCREEN width, not the window width — see
|
|
29
|
+
/// [webViewportEffectiveScale].
|
|
21
30
|
const double kWebViewportScaleTargetWidth = 1280;
|
|
22
31
|
|
|
23
|
-
///
|
|
32
|
+
/// Viewport width (logical px) where the desktop shell begins.
|
|
33
|
+
///
|
|
34
|
+
/// Below it the layout is mobile/tablet (web renders at natural 1.0); at and above
|
|
35
|
+
/// it the desktop scale applies. Uses the WINDOW width (the layout follows the
|
|
36
|
+
/// window). Tied to the responsive system's desktop breakpoint so they stay in sync.
|
|
37
|
+
const double kWebViewportScaleDesktopBreakpoint = 1024; // DeviceType.large.breakpoint
|
|
38
|
+
|
|
39
|
+
/// Effective web render scale (pure math, unit-testable — see
|
|
40
|
+
/// web_viewport_scale_test.dart).
|
|
41
|
+
///
|
|
42
|
+
/// [windowWidth] is the browser window width (drives the desktop breakpoint, since
|
|
43
|
+
/// the layout follows the window). [screenWidth] is the physical screen width in
|
|
44
|
+
/// logical px (null = unknown/native).
|
|
24
45
|
///
|
|
25
|
-
///
|
|
26
|
-
/// web
|
|
27
|
-
///
|
|
28
|
-
///
|
|
29
|
-
|
|
46
|
+
/// Returns 1.0 below the desktop breakpoint ([kWebViewportScaleDesktopBreakpoint]):
|
|
47
|
+
/// tablet/phone web render at natural size — same as native and the device preview,
|
|
48
|
+
/// so previewing a phone/tablet shows what the native build does.
|
|
49
|
+
///
|
|
50
|
+
/// On desktop it returns the flat [maxScale] cap (0.95) and only drops BELOW it
|
|
51
|
+
/// when the SCREEN is small (high OS scale), via
|
|
52
|
+
/// `screenWidth / kWebViewportScaleTargetWidth`. Keying off the screen — not the
|
|
53
|
+
/// window — is the whole point: merely resizing the browser window narrower does
|
|
54
|
+
/// NOT shrink the UI (the layout just reflows); the extra shrink happens only when
|
|
55
|
+
/// the screen itself is cramped, which is what the compensation is for. With
|
|
56
|
+
/// [screenWidth] null (native, or web before the screen is known) it stays at the
|
|
57
|
+
/// flat cap.
|
|
58
|
+
///
|
|
59
|
+
/// This is web-only semantics: native never reaches the scaling path (the
|
|
60
|
+
/// [WebViewportScale] widget short-circuits when not on web, via [kIsWeb]), so
|
|
61
|
+
/// iOS/Android and macOS/Windows apps always render at 1.0.
|
|
62
|
+
double webViewportEffectiveScale(
|
|
63
|
+
double windowWidth, {
|
|
64
|
+
double? screenWidth,
|
|
65
|
+
double maxScale = kWebViewportScale,
|
|
66
|
+
}) {
|
|
67
|
+
if (windowWidth < kWebViewportScaleDesktopBreakpoint) return 1.0;
|
|
68
|
+
final double basis = screenWidth ?? double.infinity;
|
|
69
|
+
return (basis / kWebViewportScaleTargetWidth).clamp(0.5, maxScale);
|
|
70
|
+
}
|
|
30
71
|
|
|
31
72
|
/// Renders [child] uniformly scaled by [scale] on web (no-op elsewhere).
|
|
32
73
|
///
|
|
@@ -52,16 +93,11 @@ class WebViewportScale extends StatelessWidget {
|
|
|
52
93
|
Widget build(BuildContext context) {
|
|
53
94
|
if (!kIsWeb || scale == 1.0) return child;
|
|
54
95
|
final MediaQueryData mq = MediaQuery.of(context);
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
// scale down just enough to lay the shell out at the design target width
|
|
61
|
-
// instead of cropping it. Mac (wide) stays at 0.95; Windows at 125/150/175%
|
|
62
|
-
// shrinks proportionally so both look identical.
|
|
63
|
-
final double effectiveScale =
|
|
64
|
-
(mq.size.width / kWebViewportScaleTargetWidth).clamp(0.5, scale);
|
|
96
|
+
final double effectiveScale = webViewportEffectiveScale(
|
|
97
|
+
mq.size.width,
|
|
98
|
+
screenWidth: currentScreenWidth(),
|
|
99
|
+
maxScale: scale,
|
|
100
|
+
);
|
|
65
101
|
if (effectiveScale == 1.0) return child;
|
|
66
102
|
final Size logicalSize = Size(
|
|
67
103
|
mq.size.width / effectiveScale,
|