kasy-cli 1.32.0 → 1.34.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 (60) hide show
  1. package/bin/kasy.js +42 -0
  2. package/lib/commands/apple-web.js +222 -0
  3. package/lib/commands/configure.js +3 -91
  4. package/lib/commands/doctor.js +20 -0
  5. package/lib/commands/facebook.js +189 -0
  6. package/lib/commands/new.js +50 -2
  7. package/lib/scaffold/CHANGELOG.json +18 -0
  8. package/lib/scaffold/backends/firebase/setup-from-scratch.js +164 -0
  9. package/lib/scaffold/backends/supabase/deploy.js +92 -0
  10. package/lib/scaffold/backends/supabase/patch/lib/features/authentication/api/authentication_api.dart +10 -0
  11. package/lib/scaffold/shared/generator-utils.js +18 -6
  12. package/lib/utils/apple-web.js +147 -0
  13. package/lib/utils/facebook.js +162 -0
  14. package/lib/utils/i18n/messages-en.js +62 -0
  15. package/lib/utils/i18n/messages-es.js +62 -0
  16. package/lib/utils/i18n/messages-pt.js +62 -0
  17. package/package.json +2 -2
  18. package/templates/firebase/AGENTS.md +87 -0
  19. package/templates/firebase/CLAUDE.md +16 -0
  20. package/templates/firebase/DESIGN_SYSTEM.md +234 -0
  21. package/templates/firebase/docs/auth-setup.en.md +2 -2
  22. package/templates/firebase/docs/auth-setup.es.md +2 -2
  23. package/templates/firebase/docs/auth-setup.pt.md +2 -2
  24. package/templates/firebase/lib/components/components.dart +1 -0
  25. package/templates/firebase/lib/components/kasy_app_bar.dart +4 -1
  26. package/templates/firebase/lib/components/kasy_screen.dart +114 -0
  27. package/templates/firebase/lib/components/kasy_toast.dart +39 -70
  28. package/templates/firebase/lib/core/chrome/chrome_visibility.dart +22 -0
  29. package/templates/firebase/lib/core/config/features.dart +5 -0
  30. package/templates/firebase/lib/core/rating/widgets/review_popup.dart +46 -124
  31. package/templates/firebase/lib/core/widgets/update_bottom_sheet.dart +29 -126
  32. package/templates/firebase/lib/features/ai_chat/ai_chat_page.dart +11 -7
  33. package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_chat_composer.dart +21 -0
  34. package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_conversation_list.dart +1 -1
  35. package/templates/firebase/lib/features/authentication/api/authentication_api.dart +61 -0
  36. package/templates/firebase/lib/features/authentication/ui/signin_page.dart +21 -15
  37. package/templates/firebase/lib/features/authentication/ui/signup_page.dart +20 -14
  38. package/templates/firebase/lib/features/feedbacks/ui/widgets/feature_card.dart +1 -2
  39. package/templates/firebase/lib/features/home/home_components_page.dart +7 -1
  40. package/templates/firebase/lib/features/home/home_components_preview_registry.dart +32 -0
  41. package/templates/firebase/lib/features/local_reminders/ui/reminder_page.dart +165 -209
  42. package/templates/firebase/lib/features/notifications/ui/components/notification_settings_sheet.dart +2 -2
  43. package/templates/firebase/lib/features/notifications/ui/components/notification_tile.dart +21 -8
  44. package/templates/firebase/lib/features/notifications/ui/components/push_notification_switcher.dart +2 -2
  45. package/templates/firebase/lib/features/notifications/ui/notifications_page.dart +3 -6
  46. package/templates/firebase/lib/features/notifications/ui/widgets/notification_tile.dart +77 -126
  47. package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_illustration_scaffold.dart +3 -4
  48. package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_radio_question.dart +2 -4
  49. package/templates/firebase/lib/features/onboarding/ui/widgets/selectable_row_tile.dart +2 -1
  50. package/templates/firebase/lib/features/settings/ui/components/admin/admin_page.dart +17 -8
  51. package/templates/firebase/lib/features/settings/ui/components/avatar_component.dart +2 -2
  52. package/templates/firebase/lib/i18n/en.i18n.json +5 -4
  53. package/templates/firebase/lib/i18n/es.i18n.json +5 -4
  54. package/templates/firebase/lib/i18n/pt.i18n.json +5 -4
  55. package/templates/firebase/lib/router.dart +2 -0
  56. package/templates/firebase/pubspec.yaml +1 -1
  57. package/templates/firebase/tool/design_check.dart +152 -0
  58. package/templates/firebase/assets/images/review.png +0 -0
  59. package/templates/firebase/assets/images/update.png +0 -0
  60. package/templates/firebase/lib/features/notifications/ui/components/notifications_header.dart +0 -32
@@ -0,0 +1,234 @@
1
+ # Kasy Design System
2
+
3
+ **Version 1.0.0** · last updated 2026-06-11
4
+
5
+ The single source of truth for how the app looks. Everything here lives in code
6
+ under `lib/core/theme/` and `lib/components/`, so changing a token in one place
7
+ updates the whole app (web, Android, iOS) at once. This document just writes the
8
+ rules down so anyone, including projects generated by the Kasy CLI, inherits the
9
+ same standard without guessing.
10
+
11
+ Design principle: **clean, modern, minimal**. Lots of surface, one accent colour
12
+ used sparingly, no loud weights. The Home screen (sidebar + header + elevated
13
+ `KasyCard`) is the reference for "what good looks like"; new screens should
14
+ mirror it.
15
+
16
+ ---
17
+
18
+ ## The golden rule
19
+
20
+ Use **tokens and roles**, never bespoke values.
21
+
22
+ | Instead of… | Use… |
23
+ | ----------------------------------- | -------------------------------------------- |
24
+ | `fontSize: 16, fontWeight: w600` | `context.textTheme.*` / `KasyTextTheme.*` |
25
+ | `Icon(size: 20)` | `Icon(size: KasyIconSize.lg)` |
26
+ | `EdgeInsets.all(16)` | `EdgeInsets.all(KasySpacing.md)` |
27
+ | `BorderRadius.circular(16)` | `BorderRadius.circular(KasyRadius.lg)` |
28
+ | `Color(0xFF…)` / `Colors.*` | `context.colors.*` |
29
+ | `ElevatedButton`, `TextButton` | `KasyButton` |
30
+ | `AlertDialog`, `SnackBar` | `showKasyConfirmDialog`, `showKasyToast` |
31
+
32
+ A bespoke number is only acceptable when a component is being *calibrated* (the
33
+ sidebar, the date-picker grid, badges) and there is no token that fits. When in
34
+ doubt, add or reuse a role rather than hardcoding.
35
+
36
+ ---
37
+
38
+ ## Typography — `KasyTextTheme` (`lib/core/theme/texts.dart`)
39
+
40
+ ### HeroUI scale (the raw roles, Inter)
41
+
42
+ | Role | Size / weight / line-height |
43
+ | --------------- | --------------------------- |
44
+ | `heading1` | 36 / w800 / 40 |
45
+ | `heading2` | 24 / w700 / 32 |
46
+ | `heading3` | 20 / w600 / 28 |
47
+ | `heading4` | 16 / w600 / 24 |
48
+ | `bodyBase` | 16 / w400 |
49
+ | `bodyBaseMedium`| 16 / w500 |
50
+ | `bodySm` | 14 / w400 |
51
+ | `bodySmMedium` | 14 / w500 |
52
+ | `bodyXs` | 12 / w400 |
53
+ | `bodyXsMedium` | 12 / w500 |
54
+
55
+ ### Semantic app roles (prefer these on screens)
56
+
57
+ | Role | Maps to | Use for |
58
+ | -------------- | ------------------------------ | ----------------------------------------- |
59
+ | `pageTitle` | `heading2` (24/w700) | Screen / page titles, onboarding titles |
60
+ | `sectionTitle` | `heading4` (16/w600) | Detail / section headings |
61
+ | `sectionLabel` | 12 / w600 / letter-spacing 0.5 | Uppercase group eyebrows (lists, settings)|
62
+ | `rowTitle` | `bodySmMedium` (14/w500) | List / settings row titles |
63
+ | `rowValue` | `bodySm` (14/w400) | List / settings row values |
64
+ | `cardTitle` | `bodySmMedium` (14/w500) | Card titles |
65
+ | `cardSubtitle` | `bodyXs` (12/w400) | Card secondary text |
66
+ | `caption` | `bodyXs` (12/w400) | Captions, helper text |
67
+
68
+ Access roles in widgets via `context.textTheme.*` (Material slots, colour
69
+ applied) or `KasyTextTheme.*` (raw role, apply colour with `.copyWith`).
70
+
71
+ Material → role mapping: `headlineMedium = heading2`, `headlineSmall =
72
+ titleLarge = heading3`, `titleMedium = heading4`.
73
+
74
+ The chrome/app-bar title is **w700** (not w900). Black weights read as heavy and
75
+ fight the rest of the UI.
76
+
77
+ ---
78
+
79
+ ## Icon sizes — `KasyIconSize` (`lib/core/theme/icon_sizes.dart`)
80
+
81
+ | Token | px | Token alias | = |
82
+ | ----------- | -- | ------------ | -- |
83
+ | `xxs` | 12 | `rowLeading` | `lg` (20) |
84
+ | `xs` | 14 | `rowTrailing`| `sm` (16) |
85
+ | `sm` | 16 | `chrome` | `lg` (20) |
86
+ | `md` | 18 | `inline` | `md` (18) |
87
+ | `lg` | 20 | | |
88
+ | `xl` | 24 | | |
89
+ | `xxl` | 28 | | |
90
+ | `display` | 36 | | |
91
+ | `hero` | 72 | | |
92
+
93
+ Never pass a numeric `size:` to `Icon`; use a token so re-scaling is one edit.
94
+
95
+ ---
96
+
97
+ ## Spacing — `KasySpacing` (`lib/core/theme/spacing.dart`)
98
+
99
+ `xs 4 · sm 8 · smd 12 · md 16 · lg 24 · xl 32 · xxl 48 · xxxl 64`
100
+
101
+ `pageHorizontalGutter = md (16)` · `pageVerticalGutter = sm (8)` ·
102
+ `belowChromeContentGap = 20`.
103
+
104
+ ## Radius — `KasyRadius` (`lib/core/theme/radius.dart`)
105
+
106
+ Semantic: `xs 4 · sm 8 · md 12 · lg 16 · xl 24 · full 999`.
107
+ `KasyCard` defaults to `lg` (16).
108
+
109
+ ---
110
+
111
+ ## Colours — `KasyColors`, via `context.colors.*`
112
+
113
+ Always pull colours from `context.colors.*` so light/dark resolve automatically.
114
+ Key tokens: `primary`, `onPrimary`, `surface`, `onSurface`, `onBackground`,
115
+ `muted`, `fieldLabel`, `outline`, `error` / `onError`, `success`, `warning`,
116
+ `surfacePrimarySoft`, `surfaceNeutralSoft`, `surfaceErrorSoft`.
117
+
118
+ Brand: avocado green `#D2F51E` (accent), deep green `#011820` (dark ground).
119
+ Disabled state: blend the colour toward the surface (opaque), never raw opacity.
120
+
121
+ ---
122
+
123
+ ## Components
124
+
125
+ ### `KasyButton`
126
+ Sizes (height): `small 40` · `medium 45` (default) · `large 54`. Radius
127
+ `KasyRadius.md`. Variants: primary, secondary, tertiary, destructive,
128
+ destructiveSoft, inverse, link, soft, neutral, outline, ghost.
129
+
130
+ ### `KasyTextField`
131
+ Canonical single-line height: **`singleLineHeight = 45`** (drives the field's
132
+ vertical padding so the box itself is exactly that tall on every platform — web
133
+ density is normalised to `standard`). Matches the medium button.
134
+
135
+ Variants:
136
+ - **primary** — surface fill + hairline border + soft shadow (floats; good on a
137
+ bare background).
138
+ - **secondary** — elevated fill (contrasts on a card) + border, no shadow.
139
+ - **flat** — primary's surface fill + a soft border, **no shadow**. For fields
140
+ sitting on a same-coloured card. Border comes from
141
+ `KasyShadows.inputFieldFlatBorder` (derived from `onSurface`, so it reads in
142
+ both light and dark; subtle by design).
143
+ - **embedded** — transparent, no border, no shadow.
144
+
145
+ Auth screens (sign in / sign up / recover) use **flat** on every breakpoint.
146
+ A custom `contentPadding` opts a field out of the 45 lock (e.g. the compact
147
+ header search). Use `forceFocusBorder` to show the "active" focused border from
148
+ state without taking real focus (composite triggers like the date picker).
149
+
150
+ ### `KasyTextArea`
151
+ Multi-line sibling of `KasyTextField`; same variants (primary / secondary / flat
152
+ / embedded). Height grows with content (not locked to 45).
153
+
154
+ ### `KasyDatePicker`
155
+ Reuses `KasyTextField` as a read-only trigger, so it inherits the field look and
156
+ the 45 height. The "open" border is painted from state (`forceFocusBorder`), so
157
+ a mouse click never doubles the keyboard focus ring and the border never drops
158
+ while the calendar is open.
159
+
160
+ ### `KasyCard`
161
+ Elevated surface panel, radius `KasyRadius.lg` (16). The base for screen content.
162
+
163
+ ### `KasyScreen`
164
+ Opt-in screen scaffold. A new page wrapped in it gets the internal-screen
165
+ contract for free: page background, a centred content column capped at
166
+ `maxContentWidth` (~600), the page gutter, scroll handling and an optional
167
+ `KasyCard`. It is a thin convenience over `Scaffold` (same `appBar` /
168
+ `floatingActionButton` / `bottomNavigationBar` slots), so it never blocks a
169
+ screen that needs bespoke chrome.
170
+
171
+ ```dart
172
+ KasyScreen(
173
+ appBar: KasyAppBar.root(title: 'Settings'),
174
+ card: true,
175
+ child: Column(children: [...]),
176
+ )
177
+ ```
178
+
179
+ ---
180
+
181
+ ## Internal-screen contract
182
+
183
+ New internal screens follow the sidebar / Home ruler:
184
+ - Body text 14, icons 20 (`KasyIconSize.lg`), titles ~16-17 (`sectionTitle`).
185
+ - `KasyCard` radius 16, content width contained (~600), no oversized boxes.
186
+ - Use `KasyButton`, `showKasyConfirmDialog`, `showKasyToast` — not raw Material.
187
+ - Haptics only on native (`if (!kIsWeb)`); "hide chrome on scroll" only on the
188
+ phone breakpoint (`width < 768`).
189
+
190
+ ---
191
+
192
+ ## Keeping it consistent — the guard-rail (optional)
193
+
194
+ `tool/design_check.dart` is a plain Dart script that flags a **feature screen**
195
+ reaching around the design system (raw `fontSize`, raw `Color`, raw Material
196
+ widgets, literal icon sizes). The design system's own code (`lib/components`,
197
+ `lib/core`, `lib/main.dart`) is out of scope — that is where the primitives are
198
+ built.
199
+
200
+ ```bash
201
+ dart run tool/design_check.dart
202
+ ```
203
+
204
+ It is **opt-in and non-blocking by default**: nothing runs it automatically (not
205
+ wired into `flutter analyze`, builds or git hooks). Run it locally or add it to
206
+ CI to turn the convention into a gate. It has no extra dependency — read it, tune
207
+ the allowlists at the top, or delete it. For a deliberate one-off, put
208
+ `// design-check: ignore` on the line.
209
+
210
+ This is intentionally not a cage: a project built with the kit can relax or drop
211
+ it freely, and rebranding is still a token change (colours / type) in one place.
212
+
213
+ ---
214
+
215
+ ## Changelog
216
+
217
+ ### 1.1.0 — 2026-06-11
218
+ - `KasyScreen` scaffold added (opt-in internal-screen contract).
219
+ - Design guard-rail added (`tool/design_check.dart`), opt-in / non-blocking.
220
+ - Feature layer brought to a green baseline against the guard-rail (deliberate
221
+ exceptions marked with `// design-check: ignore`).
222
+
223
+
224
+ ### 1.0.0 — 2026-06-11
225
+ - Icon sizes centralised into `KasyIconSize`; product migrated off numeric sizes.
226
+ - Semantic typography roles added (`pageTitle`, `sectionTitle`, `sectionLabel`,
227
+ `rowTitle`, …).
228
+ - `KasyTextField`: `flat` variant + `forceFocusBorder`; single-line height
229
+ locked to **45** via derived padding (platform-independent); flat border role
230
+ `inputFieldFlatBorder` (visible in both themes, subtle).
231
+ - `KasyButton` medium height aligned to **45**.
232
+ - `KasyTextArea` gained the variant set.
233
+ - App-bar title weight `w900 → w700`; onboarding titles `28/26 → 24` (page-title
234
+ scale); notifications group eyebrow → `sectionLabel`.
@@ -73,9 +73,9 @@ Requires an [Apple Developer](https://developer.apple.com) account (paid).
73
73
  >
74
74
  > **Android**: the Apple button is hidden by design (it needs the paid Services ID web flow and adds little on Android for a SaaS). Leave it hidden.
75
75
  >
76
- > **Web (Firebase)**: works after Steps 1-4 above (`withAppleWebSignin` already ships `true`). The Services ID Return URL (`firebaseapp.com/__/auth/handler`) covers the popup flow.
76
+ > **Web (Firebase)**: after Steps 1-3, run `kasy apple-web` — it writes the Services ID + Team ID + Key ID + `.p8` into the Firebase Apple provider and turns on `withAppleWebSignin` (which ships `false`). The Services ID Return URL (`firebaseapp.com/__/auth/handler`) covers the popup flow. Firebase re-signs the secret itself (never expires).
77
77
  >
78
- > **Web (Supabase)**: the CLI ships `withAppleWebSignin = false` (native iOS works; web needs more). To enable it: in Supabase Authentication Providers Apple, add the **client secret** (a JWT signed with your `.p8` key + Services ID), then set `withAppleWebSignin = true` in `lib/core/config/features.dart`.
78
+ > **Web (Supabase)**: Apple on the web for Supabase is **not wired in the app yet** (roadmap) the button stays hidden on web. On Supabase, Apple works on **native iOS only** (already set up by `kasy new`). See `ROADMAP.md`.
79
79
 
80
80
  ---
81
81
 
@@ -73,9 +73,9 @@ Requiere cuenta de [Apple Developer](https://developer.apple.com) (de pago).
73
73
  >
74
74
  > **Android**: el botón Apple queda oculto por defecto (necesita el flujo del Services ID de pago y aporta poco en Android para un SaaS). Déjalo oculto.
75
75
  >
76
- > **Web (Firebase)**: funciona tras los Pasos 1 a 4 anteriores (`withAppleWebSignin` ya viene `true`). La Return URL del Services ID (`firebaseapp.com/__/auth/handler`) cubre el flujo de popup.
76
+ > **Web (Firebase)**: tras los Pasos 1 a 3, ejecuta `kasy apple-web` — escribe el Services ID + Team ID + Key ID + `.p8` en el proveedor Apple de Firebase y activa `withAppleWebSignin` (que viene `false`). La Return URL del Services ID (`firebaseapp.com/__/auth/handler`) cubre el flujo de popup. Firebase vuelve a firmar el secret solo (no expira).
77
77
  >
78
- > **Web (Supabase)**: la CLI genera `withAppleWebSignin = false` (iOS nativo funciona; la web necesita más). Para habilitar: en Supabase Authentication Providers Apple, agrega el **client secret** (un JWT firmado con la clave `.p8` + Services ID) y luego define `withAppleWebSignin = true` en `lib/core/config/features.dart`.
78
+ > **Web (Supabase)**: Apple en la web para Supabase **aún no viene activado en la app** (roadmap) el botón queda oculto en la web. En Supabase, Apple funciona solo en **iOS nativo** (ya configurado por `kasy new`). Consulta `ROADMAP.md`.
79
79
 
80
80
  ---
81
81
 
@@ -73,9 +73,9 @@ Requer conta [Apple Developer](https://developer.apple.com) (paga).
73
73
  >
74
74
  > **Android**: o botão Apple fica escondido por padrão (exige o fluxo do Services ID pago e agrega pouco no Android para um SaaS). Deixe escondido.
75
75
  >
76
- > **Web (Firebase)**: funciona depois dos Passos 1 a 4 acima (`withAppleWebSignin` vem `true`). A Return URL do Services ID (`firebaseapp.com/__/auth/handler`) cobre o fluxo de popup.
76
+ > **Web (Firebase)**: depois dos Passos 1 a 3, rode `kasy apple-web` — ele grava o Services ID + Team ID + Key ID + `.p8` no provedor Apple do Firebase e liga `withAppleWebSignin` (que nasce `false`). A Return URL do Services ID (`firebaseapp.com/__/auth/handler`) cobre o fluxo de popup. O Firebase re-assina o secret sozinho (não expira).
77
77
  >
78
- > **Web (Supabase)**: a CLI gera `withAppleWebSignin = false` (iOS nativo funciona; web precisa de mais). Para habilitar: em Supabase Authentication Providers Apple, adicione o **client secret** (um JWT assinado com a chave `.p8` + Services ID) e depois defina `withAppleWebSignin = true` em `lib/core/config/features.dart`.
78
+ > **Web (Supabase)**: Apple na web no Supabase **não vem ligado no app ainda** (roadmap) o botão fica escondido na web. No Supabase, o Apple funciona no **iOS nativo** ( configurado pelo `kasy new`). Veja `ROADMAP.md`.
79
79
 
80
80
  ---
81
81
 
@@ -21,6 +21,7 @@ export 'kasy_date_picker.dart';
21
21
  export 'kasy_dialog.dart';
22
22
  export 'kasy_image_viewer.dart';
23
23
  export 'kasy_otp_verification_bottom_sheet.dart';
24
+ export 'kasy_screen.dart';
24
25
  export 'kasy_sidebar.dart';
25
26
  export 'kasy_skeleton.dart';
26
27
  export 'kasy_status_tag.dart';
@@ -304,7 +304,10 @@ class KasyAppBar extends StatelessWidget {
304
304
  context.textTheme.headlineSmall ?? context.textTheme.titleLarge;
305
305
  final TextStyle? titleStyle = titleRef?.copyWith(
306
306
  fontSize: (titleRef.fontSize ?? 24) * kasyAppBarTitleFontScale,
307
- fontWeight: FontWeight.w900,
307
+ // w700 (not w900): a clean page-title weight that matches the Home /
308
+ // sidebar scale. w900 read as a heavy "black" that fought the rest of
309
+ // the chrome.
310
+ fontWeight: FontWeight.w700,
308
311
  letterSpacing: -0.35,
309
312
  color: context.colors.onBackground,
310
313
  );
@@ -0,0 +1,114 @@
1
+ import 'package:flutter/material.dart';
2
+ import 'package:kasy_kit/components/kasy_card.dart';
3
+ import 'package:kasy_kit/core/theme/theme.dart';
4
+ import 'package:kasy_kit/core/widgets/page_background.dart';
5
+
6
+ /// Canonical screen scaffold. A new page that uses it inherits the internal
7
+ /// screen contract for free: the page background, a centred content column
8
+ /// capped at [maxContentWidth], the page gutter, scroll handling, and
9
+ /// (optionally) a [KasyCard] wrapper — all from design-system tokens.
10
+ ///
11
+ /// Opt-in by design: it is a thin convenience over [Scaffold] (it forwards the
12
+ /// same `appBar`, `floatingActionButton`, `bottomNavigationBar` slots), not a
13
+ /// cage. Screens that need bespoke chrome can keep using [Scaffold] directly —
14
+ /// nothing here is forced, and any value is overridable.
15
+ ///
16
+ /// ```dart
17
+ /// KasyScreen(
18
+ /// appBar: KasyAppBar.root(title: 'Settings'),
19
+ /// card: true,
20
+ /// child: Column(children: [...]),
21
+ /// )
22
+ /// ```
23
+ class KasyScreen extends StatelessWidget {
24
+ const KasyScreen({
25
+ super.key,
26
+ required this.child,
27
+ this.appBar,
28
+ this.scrollable = true,
29
+ this.maxContentWidth = 600,
30
+ this.padding,
31
+ this.card = false,
32
+ this.backgroundColor,
33
+ this.resizeToAvoidBottomInset,
34
+ this.floatingActionButton,
35
+ this.bottomNavigationBar,
36
+ });
37
+
38
+ /// Page content.
39
+ final Widget child;
40
+
41
+ /// Optional top bar. Same slot as [Scaffold.appBar]; pass any
42
+ /// [PreferredSizeWidget].
43
+ final PreferredSizeWidget? appBar;
44
+
45
+ /// Wrap the content in a scroll view (default true). Set false for screens
46
+ /// that manage their own scrolling (e.g. a full-bleed list).
47
+ final bool scrollable;
48
+
49
+ /// Max width of the content column on wide screens. The internal-screen
50
+ /// contract is ~600; content is centred within the available width so it does
51
+ /// not stretch edge-to-edge on web/desktop.
52
+ final double maxContentWidth;
53
+
54
+ /// Padding around the content. Defaults to the horizontal page gutter plus
55
+ /// vertical breathing room.
56
+ final EdgeInsetsGeometry? padding;
57
+
58
+ /// Wrap the content in an elevated [KasyCard] (radius 16), matching the Home
59
+ /// reference look.
60
+ final bool card;
61
+
62
+ /// Page background. Defaults to the theme background token.
63
+ final Color? backgroundColor;
64
+
65
+ /// Forwarded to [Scaffold.resizeToAvoidBottomInset].
66
+ final bool? resizeToAvoidBottomInset;
67
+
68
+ /// Forwarded to [Scaffold.floatingActionButton].
69
+ final Widget? floatingActionButton;
70
+
71
+ /// Forwarded to [Scaffold.bottomNavigationBar].
72
+ final Widget? bottomNavigationBar;
73
+
74
+ @override
75
+ Widget build(BuildContext context) {
76
+ final EdgeInsetsGeometry resolvedPadding = padding ??
77
+ const EdgeInsets.symmetric(
78
+ horizontal: KasySpacing.pageHorizontalGutter,
79
+ vertical: KasySpacing.lg,
80
+ );
81
+
82
+ Widget content = ConstrainedBox(
83
+ constraints: BoxConstraints(maxWidth: maxContentWidth),
84
+ child: card ? KasyCard(child: child) : child,
85
+ );
86
+
87
+ content = Align(
88
+ alignment: Alignment.topCenter,
89
+ child: Padding(padding: resolvedPadding, child: content),
90
+ );
91
+
92
+ if (scrollable) {
93
+ content = SingleChildScrollView(
94
+ physics: const ClampingScrollPhysics(),
95
+ keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag,
96
+ child: content,
97
+ );
98
+ }
99
+
100
+ return Scaffold(
101
+ backgroundColor: backgroundColor ?? context.colors.background,
102
+ resizeToAvoidBottomInset: resizeToAvoidBottomInset,
103
+ appBar: appBar,
104
+ floatingActionButton: floatingActionButton,
105
+ bottomNavigationBar: bottomNavigationBar,
106
+ body: Background(
107
+ bgColor: backgroundColor,
108
+ // top:false when an app bar is present — Scaffold already insets the
109
+ // body below it, so SafeArea only needs to guard the bottom/sides.
110
+ child: SafeArea(top: appBar == null, child: content),
111
+ ),
112
+ );
113
+ }
114
+ }
@@ -52,29 +52,36 @@ class KasyToast extends StatelessWidget {
52
52
  color: c.surface,
53
53
  borderRadius: KasyRadius.lgBorderRadius,
54
54
  border: Border.all(
55
- color: c.outline.withValues(alpha: isDark ? 0.22 : 0.28),
55
+ color: c.outline.withValues(alpha: isDark ? 0.20 : 0.20),
56
56
  ),
57
57
  boxShadow: [
58
58
  BoxShadow(
59
- color: Colors.black.withValues(alpha: isDark ? 0.12 : 0.04),
60
- blurRadius: 10,
61
- offset: const Offset(0, 4),
59
+ color: Colors.black.withValues(alpha: isDark ? 0.22 : 0.06),
60
+ blurRadius: 18,
61
+ offset: const Offset(0, 6),
62
62
  ),
63
63
  ],
64
64
  ),
65
65
  child: Padding(
66
- padding: const EdgeInsets.symmetric(
67
- horizontal: KasySpacing.md,
68
- vertical: 14,
66
+ // Tighter vertical rhythm and less right padding (the close affordance
67
+ // is a light icon, not a filled button) for a slimmer, refined bar.
68
+ padding: const EdgeInsets.fromLTRB(
69
+ KasySpacing.md,
70
+ KasySpacing.smd,
71
+ KasySpacing.sm,
72
+ KasySpacing.smd,
69
73
  ),
70
74
  child: Row(
71
75
  crossAxisAlignment: CrossAxisAlignment.start,
72
76
  children: [
73
77
  Padding(
74
- padding: const EdgeInsets.only(top: 2),
75
- child: icon ?? Icon(pal.icon, size: KasyIconSize.xl, color: pal.accent),
78
+ padding: const EdgeInsets.only(top: 1),
79
+ // Only the icon carries the tone colour; the title stays neutral
80
+ // so the toast reads calm instead of shouting.
81
+ child: icon ??
82
+ Icon(pal.icon, size: KasyIconSize.lg, color: pal.accent),
76
83
  ),
77
- const SizedBox(width: KasySpacing.smd),
84
+ const SizedBox(width: KasySpacing.sm),
78
85
  Expanded(
79
86
  child: Column(
80
87
  crossAxisAlignment: CrossAxisAlignment.start,
@@ -82,38 +89,34 @@ class KasyToast extends StatelessWidget {
82
89
  children: [
83
90
  Text(
84
91
  title,
85
- style: context.textTheme.titleMedium?.copyWith(
86
- color: pal.accent,
87
- fontWeight: FontWeight.w700,
92
+ style: context.textTheme.titleSmall?.copyWith(
93
+ color: c.onSurface,
94
+ fontWeight: FontWeight.w600,
88
95
  ),
89
96
  ),
90
97
  if (message != null) ...[
91
- const SizedBox(height: 4),
98
+ const SizedBox(height: 2),
92
99
  Text(
93
100
  message!,
94
101
  style: context.textTheme.bodyMedium?.copyWith(
95
- // Slightly darker than muted in light, slightly lighter in dark.
96
- color: Color.lerp(
97
- c.muted,
98
- isDark ? Colors.white : Colors.black,
99
- 0.18,
100
- ),
102
+ color: c.muted,
103
+ height: 1.35,
101
104
  ),
102
105
  ),
103
106
  ],
104
107
  ],
105
108
  ),
106
109
  ),
107
- const SizedBox(width: KasySpacing.smd),
108
- Padding(
109
- padding: const EdgeInsets.only(top: 1),
110
- child: KasyButton(
111
- label: 'Close',
112
- size: KasyButtonSize.small,
113
- backgroundColor: pal.buttonBg,
114
- foregroundColor: pal.buttonFg,
115
- onPressed: onClose,
116
- ),
110
+ const SizedBox(width: KasySpacing.xs),
111
+ KasyButton.iconOnly(
112
+ icon: KasyIcons.close,
113
+ variant: KasyButtonVariant.ghost,
114
+ foregroundColor: c.muted,
115
+ size: KasyButtonSize.small,
116
+ iconOnlyLayoutExtent: 28,
117
+ iconGlyphSize: 16,
118
+ onPressed: onClose,
119
+ semanticLabel: MaterialLocalizations.of(context).closeButtonTooltip,
117
120
  ),
118
121
  ],
119
122
  ),
@@ -706,63 +709,29 @@ class _AnimatedToastState extends State<_AnimatedToast>
706
709
  // ---------------------------------------------------------------------------
707
710
 
708
711
  class _Palette {
709
- const _Palette({
710
- required this.accent,
711
- required this.icon,
712
- required this.buttonBg,
713
- required this.buttonFg,
714
- });
712
+ const _Palette({required this.accent, required this.icon});
715
713
 
716
714
  final Color accent;
717
715
  final IconData icon;
718
- final Color buttonBg;
719
- final Color buttonFg;
720
716
  }
721
717
 
722
718
  _Palette _palette(KasyColors c, KasyToastTone tone) {
723
719
  switch (tone) {
724
720
  case KasyToastTone.neutral:
725
- return _Palette(
726
- accent: c.onSurface,
727
- icon: KasyIcons.notification,
728
- buttonBg: c.surfaceNeutralSoft,
729
- buttonFg: c.onSurface,
730
- );
721
+ return _Palette(accent: c.onSurface, icon: KasyIcons.notification);
731
722
  case KasyToastTone.accent:
732
- return _Palette(
733
- accent: c.primary,
734
- icon: KasyIcons.info,
735
- buttonBg: c.primary,
736
- // The Close button sits on a solid, vivid tone color — keep its label
737
- // white for reliable contrast regardless of the brand "on" token.
738
- buttonFg: Colors.white,
739
- );
723
+ return _Palette(accent: c.primary, icon: KasyIcons.info);
740
724
  case KasyToastTone.success:
741
725
  final Color successDark = HSLColor.fromColor(c.success)
742
726
  .withLightness(
743
727
  (HSLColor.fromColor(c.success).lightness - 0.09).clamp(0.0, 1.0),
744
728
  )
745
729
  .toColor();
746
- return _Palette(
747
- accent: successDark,
748
- icon: KasyIcons.checkCircle,
749
- buttonBg: successDark,
750
- buttonFg: Colors.white,
751
- );
730
+ return _Palette(accent: successDark, icon: KasyIcons.checkCircle);
752
731
  case KasyToastTone.warning:
753
- return _Palette(
754
- accent: c.warning,
755
- icon: KasyIcons.privacy,
756
- buttonBg: c.warning,
757
- buttonFg: Colors.white,
758
- );
732
+ return _Palette(accent: c.warning, icon: KasyIcons.privacy);
759
733
  case KasyToastTone.danger:
760
- return _Palette(
761
- accent: c.error,
762
- icon: KasyIcons.error,
763
- buttonBg: c.error,
764
- buttonFg: Colors.white,
765
- );
734
+ return _Palette(accent: c.error, icon: KasyIcons.error);
766
735
  }
767
736
  }
768
737
 
@@ -71,6 +71,28 @@ class KasyChromeVisibility {
71
71
  }
72
72
  }
73
73
 
74
+ /// Brings the chrome back whenever a route is pushed onto or popped from the
75
+ /// root navigator. Tab switches already reset via [BartScaffold.onRouteChanged],
76
+ /// but detail screens (feedback, reminders, …) are pushed on the root navigator
77
+ /// and bypass that — so without this, returning to a screen that had scrolled
78
+ /// its chrome away would leave the app bar and bottom menu stuck hidden.
79
+ ///
80
+ /// Only the chrome is restored; the destination screen keeps its scroll position
81
+ /// (the framework preserves it), matching how large apps handle back navigation.
82
+ class KasyChromeVisibilityObserver extends NavigatorObserver {
83
+ void _reset() => KasyChromeVisibility.instance.resetShown();
84
+
85
+ @override
86
+ void didPush(Route<dynamic> route, Route<dynamic>? previousRoute) => _reset();
87
+
88
+ @override
89
+ void didPop(Route<dynamic> route, Route<dynamic>? previousRoute) => _reset();
90
+
91
+ @override
92
+ void didReplace({Route<dynamic>? newRoute, Route<dynamic>? oldRoute}) =>
93
+ _reset();
94
+ }
95
+
74
96
  // ---------------------------------------------------------------------------
75
97
  // Configuration — edit these to change the experience your app ships with.
76
98
  // ---------------------------------------------------------------------------
@@ -12,6 +12,11 @@ const bool withLocalReminders = true;
12
12
  /// the CLI generates them as `false`. Apple shows on iOS/macOS regardless; it is
13
13
  /// hidden on Android (also needs a paid Service ID, and the native flow throws).
14
14
  const bool withAppleWebSignin = true;
15
+ /// When true, the Facebook sign-in button is shown on WEB. Facebook-on-web works
16
+ /// on the Firebase backend (signInWithPopup) after `kasy facebook`; on Supabase the
17
+ /// web flow isn't wired yet (roadmap), so the CLI ships this `false` there. Native
18
+ /// (iOS/Android) always shows the Facebook button regardless of this flag.
19
+ const bool withFacebookWebSignin = false;
15
20
  /// When true, the app includes web support:
16
21
  /// - anonymous sign-up is disabled on web (user is redirected to /signin)
17
22
  /// - onboarding is skipped on web