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.
- package/bin/kasy.js +42 -0
- 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 +50 -2
- package/lib/scaffold/CHANGELOG.json +18 -0
- package/lib/scaffold/backends/firebase/setup-from-scratch.js +164 -0
- package/lib/scaffold/backends/supabase/deploy.js +92 -0
- package/lib/scaffold/backends/supabase/patch/lib/features/authentication/api/authentication_api.dart +10 -0
- 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 +62 -0
- package/lib/utils/i18n/messages-es.js +62 -0
- package/lib/utils/i18n/messages-pt.js +62 -0
- package/package.json +2 -2
- package/templates/firebase/AGENTS.md +87 -0
- package/templates/firebase/CLAUDE.md +16 -0
- package/templates/firebase/DESIGN_SYSTEM.md +234 -0
- package/templates/firebase/docs/auth-setup.en.md +2 -2
- package/templates/firebase/docs/auth-setup.es.md +2 -2
- package/templates/firebase/docs/auth-setup.pt.md +2 -2
- package/templates/firebase/lib/components/components.dart +1 -0
- package/templates/firebase/lib/components/kasy_app_bar.dart +4 -1
- package/templates/firebase/lib/components/kasy_screen.dart +114 -0
- package/templates/firebase/lib/components/kasy_toast.dart +39 -70
- 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/rating/widgets/review_popup.dart +46 -124
- package/templates/firebase/lib/core/widgets/update_bottom_sheet.dart +29 -126
- package/templates/firebase/lib/features/ai_chat/ai_chat_page.dart +11 -7
- 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_conversation_list.dart +1 -1
- package/templates/firebase/lib/features/authentication/api/authentication_api.dart +61 -0
- package/templates/firebase/lib/features/authentication/ui/signin_page.dart +21 -15
- package/templates/firebase/lib/features/authentication/ui/signup_page.dart +20 -14
- package/templates/firebase/lib/features/feedbacks/ui/widgets/feature_card.dart +1 -2
- package/templates/firebase/lib/features/home/home_components_page.dart +7 -1
- package/templates/firebase/lib/features/home/home_components_preview_registry.dart +32 -0
- package/templates/firebase/lib/features/local_reminders/ui/reminder_page.dart +165 -209
- 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 -6
- package/templates/firebase/lib/features/notifications/ui/widgets/notification_tile.dart +77 -126
- 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/ui/components/admin/admin_page.dart +17 -8
- package/templates/firebase/lib/features/settings/ui/components/avatar_component.dart +2 -2
- package/templates/firebase/lib/i18n/en.i18n.json +5 -4
- package/templates/firebase/lib/i18n/es.i18n.json +5 -4
- package/templates/firebase/lib/i18n/pt.i18n.json +5 -4
- package/templates/firebase/lib/router.dart +2 -0
- package/templates/firebase/pubspec.yaml +1 -1
- package/templates/firebase/tool/design_check.dart +152 -0
- package/templates/firebase/assets/images/review.png +0 -0
- package/templates/firebase/assets/images/update.png +0 -0
- 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)**:
|
|
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
|
|
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)**:
|
|
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
|
|
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)**:
|
|
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)**:
|
|
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 só no **iOS nativo** (já 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
|
-
|
|
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.
|
|
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.
|
|
60
|
-
blurRadius:
|
|
61
|
-
offset: const Offset(0,
|
|
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
|
|
67
|
-
|
|
68
|
-
|
|
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:
|
|
75
|
-
|
|
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.
|
|
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.
|
|
86
|
-
color:
|
|
87
|
-
fontWeight: FontWeight.
|
|
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:
|
|
98
|
+
const SizedBox(height: 2),
|
|
92
99
|
Text(
|
|
93
100
|
message!,
|
|
94
101
|
style: context.textTheme.bodyMedium?.copyWith(
|
|
95
|
-
|
|
96
|
-
|
|
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.
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|